1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 runnables::RunnableTasks,
9 scroll::scroll_amount::ScrollAmount,
10 test::{
11 assert_text_with_selections, build_editor, editor_content_with_blocks,
12 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
13 editor_test_context::EditorTestContext,
14 select_ranges,
15 },
16};
17use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
18use collections::HashMap;
19use futures::{StreamExt, channel::oneshot};
20use gpui::{
21 BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext,
22 WindowBounds, WindowOptions, div,
23};
24use indoc::indoc;
25use language::{
26 BracketPair, BracketPairConfig,
27 Capability::ReadWrite,
28 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
29 LanguageConfigOverride, LanguageMatcher, LanguageName, LanguageQueries, Override, Point,
30 language_settings::{
31 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
32 },
33 tree_sitter_python,
34};
35use language_settings::Formatter;
36use languages::markdown_lang;
37use languages::rust_lang;
38use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT};
39use multi_buffer::{IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey};
40use parking_lot::Mutex;
41use pretty_assertions::{assert_eq, assert_ne};
42use project::{
43 FakeFs, Project,
44 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
45 project_settings::LspSettings,
46 trusted_worktrees::{PathTrust, TrustedWorktrees},
47};
48use serde_json::{self, json};
49use settings::{
50 AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent,
51 IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent,
52 ProjectSettingsContent, ScrollBeyondLastLine, SearchSettingsContent, SettingsContent,
53 SettingsStore,
54};
55use std::borrow::Cow;
56use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
57use std::{
58 iter,
59 sync::atomic::{self, AtomicUsize},
60};
61use test::build_editor_with_project;
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};
68use workspace::{
69 CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions,
70 ViewId,
71 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
72 register_project_item,
73};
74
75fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
76 editor
77 .selections
78 .display_ranges(&editor.display_snapshot(cx))
79}
80
81#[cfg(any(test, feature = "test-support"))]
82pub mod property_test;
83
84#[gpui::test]
85fn test_edit_events(cx: &mut TestAppContext) {
86 init_test(cx, |_| {});
87
88 let buffer = cx.new(|cx| {
89 let mut buffer = language::Buffer::local("123456", cx);
90 buffer.set_group_interval(Duration::from_secs(1));
91 buffer
92 });
93
94 let events = Rc::new(RefCell::new(Vec::new()));
95 let editor1 = cx.add_window({
96 let events = events.clone();
97 |window, cx| {
98 let entity = cx.entity();
99 cx.subscribe_in(
100 &entity,
101 window,
102 move |_, _, event: &EditorEvent, _, _| match event {
103 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
104 EditorEvent::BufferEdited => {
105 events.borrow_mut().push(("editor1", "buffer edited"))
106 }
107 _ => {}
108 },
109 )
110 .detach();
111 Editor::for_buffer(buffer.clone(), None, window, cx)
112 }
113 });
114
115 let editor2 = cx.add_window({
116 let events = events.clone();
117 |window, cx| {
118 cx.subscribe_in(
119 &cx.entity(),
120 window,
121 move |_, _, event: &EditorEvent, _, _| match event {
122 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
123 EditorEvent::BufferEdited => {
124 events.borrow_mut().push(("editor2", "buffer edited"))
125 }
126 _ => {}
127 },
128 )
129 .detach();
130 Editor::for_buffer(buffer.clone(), None, window, cx)
131 }
132 });
133
134 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
135
136 // Mutating editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Mutating editor 2 will emit an `Edited` event only for that editor.
148 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor2", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 1 will emit an `Edited` event only for that editor.
159 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor1", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 1 will emit an `Edited` event only for that editor.
170 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor1", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // Undoing on editor 2 will emit an `Edited` event only for that editor.
181 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
182 assert_eq!(
183 mem::take(&mut *events.borrow_mut()),
184 [
185 ("editor2", "edited"),
186 ("editor1", "buffer edited"),
187 ("editor2", "buffer edited"),
188 ]
189 );
190
191 // Redoing on editor 2 will emit an `Edited` event only for that editor.
192 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
193 assert_eq!(
194 mem::take(&mut *events.borrow_mut()),
195 [
196 ("editor2", "edited"),
197 ("editor1", "buffer edited"),
198 ("editor2", "buffer edited"),
199 ]
200 );
201
202 // No event is emitted when the mutation is a no-op.
203 _ = editor2.update(cx, |editor, window, cx| {
204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
205 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
206 });
207
208 editor.backspace(&Backspace, window, cx);
209 });
210 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
211}
212
213#[gpui::test]
214fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
215 init_test(cx, |_| {});
216
217 let mut now = Instant::now();
218 let group_interval = Duration::from_millis(1);
219 let buffer = cx.new(|cx| {
220 let mut buf = language::Buffer::local("123456", cx);
221 buf.set_group_interval(group_interval);
222 buf
223 });
224 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
225 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
226
227 _ = editor.update(cx, |editor, window, cx| {
228 editor.start_transaction_at(now, window, cx);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
231 });
232
233 editor.insert("cd", window, cx);
234 editor.end_transaction_at(now, cx);
235 assert_eq!(editor.text(cx), "12cd56");
236 assert_eq!(
237 editor.selections.ranges(&editor.display_snapshot(cx)),
238 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
239 );
240
241 editor.start_transaction_at(now, window, cx);
242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
243 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
244 });
245 editor.insert("e", window, cx);
246 editor.end_transaction_at(now, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(
249 editor.selections.ranges(&editor.display_snapshot(cx)),
250 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
251 );
252
253 now += group_interval + Duration::from_millis(1);
254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
255 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
256 });
257
258 // Simulate an edit in another editor
259 buffer.update(cx, |buffer, cx| {
260 buffer.start_transaction_at(now, cx);
261 buffer.edit(
262 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
263 None,
264 cx,
265 );
266 buffer.edit(
267 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
268 None,
269 cx,
270 );
271 buffer.end_transaction_at(now, cx);
272 });
273
274 assert_eq!(editor.text(cx), "ab2cde6");
275 assert_eq!(
276 editor.selections.ranges(&editor.display_snapshot(cx)),
277 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
278 );
279
280 // Last transaction happened past the group interval in a different editor.
281 // Undo it individually and don't restore selections.
282 editor.undo(&Undo, window, cx);
283 assert_eq!(editor.text(cx), "12cde6");
284 assert_eq!(
285 editor.selections.ranges(&editor.display_snapshot(cx)),
286 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
287 );
288
289 // First two transactions happened within the group interval in this editor.
290 // Undo them together and restore selections.
291 editor.undo(&Undo, window, cx);
292 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
293 assert_eq!(editor.text(cx), "123456");
294 assert_eq!(
295 editor.selections.ranges(&editor.display_snapshot(cx)),
296 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
297 );
298
299 // Redo the first two transactions together.
300 editor.redo(&Redo, window, cx);
301 assert_eq!(editor.text(cx), "12cde6");
302 assert_eq!(
303 editor.selections.ranges(&editor.display_snapshot(cx)),
304 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
305 );
306
307 // Redo the last transaction on its own.
308 editor.redo(&Redo, window, cx);
309 assert_eq!(editor.text(cx), "ab2cde6");
310 assert_eq!(
311 editor.selections.ranges(&editor.display_snapshot(cx)),
312 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
313 );
314
315 // Test empty transactions.
316 editor.start_transaction_at(now, window, cx);
317 editor.end_transaction_at(now, cx);
318 editor.undo(&Undo, window, cx);
319 assert_eq!(editor.text(cx), "12cde6");
320 });
321}
322
323#[gpui::test]
324fn test_accessibility_keyboard_word_completion(cx: &mut TestAppContext) {
325 init_test(cx, |_| {});
326
327 // Simulates the macOS Accessibility Keyboard word completion panel, which calls
328 // insertText:replacementRange: to commit a completion. macOS sends two calls per
329 // completion: one with a non-empty range replacing the typed prefix, and one with
330 // an empty replacement range (cursor..cursor) to append a trailing space.
331
332 cx.add_window(|window, cx| {
333 let buffer = MultiBuffer::build_simple("ab", cx);
334 let mut editor = build_editor(buffer, window, cx);
335
336 // Cursor is after the 2-char prefix "ab" at offset 2.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
339 });
340
341 // macOS completes "about" by replacing the prefix via range 0..2.
342 editor.replace_text_in_range(Some(0..2), "about", window, cx);
343 assert_eq!(editor.text(cx), "about");
344
345 // macOS sends a trailing space as an empty replacement range (cursor..cursor).
346 // Must insert at the cursor position, not call backspace first (which would
347 // delete the preceding character).
348 editor.replace_text_in_range(Some(5..5), " ", window, cx);
349 assert_eq!(editor.text(cx), "about ");
350
351 editor
352 });
353
354 // Multi-cursor: the replacement must fan out to all cursors, and the trailing
355 // space must land at each cursor's actual current position. After the first
356 // completion, macOS's reported cursor offset is stale (it doesn't account for
357 // the offset shift caused by the other cursor's insertion), so the empty
358 // replacement range must be ignored and the space inserted at each real cursor.
359 cx.add_window(|window, cx| {
360 // Two cursors, each after a 2-char prefix "ab" at the end of each line:
361 // "ab\nab" — cursors at offsets 2 and 5.
362 let buffer = MultiBuffer::build_simple("ab\nab", cx);
363 let mut editor = build_editor(buffer, window, cx);
364
365 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
366 s.select_ranges([
367 MultiBufferOffset(2)..MultiBufferOffset(2),
368 MultiBufferOffset(5)..MultiBufferOffset(5),
369 ])
370 });
371
372 // macOS reports the newest cursor (offset 5) and sends range 3..5 to
373 // replace its 2-char prefix. selection_replacement_ranges applies the same
374 // delta to fan out to both cursors: 0..2 and 3..5.
375 editor.replace_text_in_range(Some(3..5), "about", window, cx);
376 assert_eq!(editor.text(cx), "about\nabout");
377
378 // Trailing space via empty range. macOS thinks the cursor is at offset 10
379 // (5 - 2 + 7 = 10), but the actual cursors are at 5 and 11. The stale
380 // offset must be ignored and the space inserted at each real cursor position.
381 editor.replace_text_in_range(Some(10..10), " ", window, cx);
382 assert_eq!(editor.text(cx), "about \nabout ");
383
384 editor
385 });
386}
387
388#[gpui::test]
389fn test_ime_composition(cx: &mut TestAppContext) {
390 init_test(cx, |_| {});
391
392 let buffer = cx.new(|cx| {
393 let mut buffer = language::Buffer::local("abcde", cx);
394 // Ensure automatic grouping doesn't occur.
395 buffer.set_group_interval(Duration::ZERO);
396 buffer
397 });
398
399 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
400 cx.add_window(|window, cx| {
401 let mut editor = build_editor(buffer.clone(), window, cx);
402
403 // Start a new IME composition.
404 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
405 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
406 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
407 assert_eq!(editor.text(cx), "äbcde");
408 assert_eq!(
409 editor.marked_text_ranges(cx),
410 Some(vec![
411 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
412 ])
413 );
414
415 // Finalize IME composition.
416 editor.replace_text_in_range(None, "ā", window, cx);
417 assert_eq!(editor.text(cx), "ābcde");
418 assert_eq!(editor.marked_text_ranges(cx), None);
419
420 // IME composition edits are grouped and are undone/redone at once.
421 editor.undo(&Default::default(), window, cx);
422 assert_eq!(editor.text(cx), "abcde");
423 assert_eq!(editor.marked_text_ranges(cx), None);
424 editor.redo(&Default::default(), window, cx);
425 assert_eq!(editor.text(cx), "ābcde");
426 assert_eq!(editor.marked_text_ranges(cx), None);
427
428 // Start a new IME composition.
429 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
430 assert_eq!(
431 editor.marked_text_ranges(cx),
432 Some(vec![
433 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
434 ])
435 );
436
437 // Undoing during an IME composition cancels it.
438 editor.undo(&Default::default(), window, cx);
439 assert_eq!(editor.text(cx), "ābcde");
440 assert_eq!(editor.marked_text_ranges(cx), None);
441
442 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
443 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
444 assert_eq!(editor.text(cx), "ābcdè");
445 assert_eq!(
446 editor.marked_text_ranges(cx),
447 Some(vec![
448 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
449 ])
450 );
451
452 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
453 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
454 assert_eq!(editor.text(cx), "ābcdę");
455 assert_eq!(editor.marked_text_ranges(cx), None);
456
457 // Start a new IME composition with multiple cursors.
458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
459 s.select_ranges([
460 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
461 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
462 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
463 ])
464 });
465 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
466 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
467 assert_eq!(
468 editor.marked_text_ranges(cx),
469 Some(vec![
470 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
471 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
472 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
473 ])
474 );
475
476 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
477 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
478 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
479 assert_eq!(
480 editor.marked_text_ranges(cx),
481 Some(vec![
482 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
483 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
484 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
485 ])
486 );
487
488 // Finalize IME composition with multiple cursors.
489 editor.replace_text_in_range(Some(9..10), "2", window, cx);
490 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
491 assert_eq!(editor.marked_text_ranges(cx), None);
492
493 editor
494 });
495}
496
497#[gpui::test]
498fn test_selection_with_mouse(cx: &mut TestAppContext) {
499 init_test(cx, |_| {});
500
501 let editor = cx.add_window(|window, cx| {
502 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
503 build_editor(buffer, window, cx)
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
508 });
509 assert_eq!(
510 editor
511 .update(cx, |editor, _, cx| display_ranges(editor, cx))
512 .unwrap(),
513 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
514 );
515
516 _ = editor.update(cx, |editor, window, cx| {
517 editor.update_selection(
518 DisplayPoint::new(DisplayRow(3), 3),
519 0,
520 gpui::Point::<f32>::default(),
521 window,
522 cx,
523 );
524 });
525
526 assert_eq!(
527 editor
528 .update(cx, |editor, _, cx| display_ranges(editor, cx))
529 .unwrap(),
530 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
531 );
532
533 _ = editor.update(cx, |editor, window, cx| {
534 editor.update_selection(
535 DisplayPoint::new(DisplayRow(1), 1),
536 0,
537 gpui::Point::<f32>::default(),
538 window,
539 cx,
540 );
541 });
542
543 assert_eq!(
544 editor
545 .update(cx, |editor, _, cx| display_ranges(editor, cx))
546 .unwrap(),
547 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
548 );
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.end_selection(window, cx);
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(3), 3),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 });
560
561 assert_eq!(
562 editor
563 .update(cx, |editor, _, cx| display_ranges(editor, cx))
564 .unwrap(),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
566 );
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
570 editor.update_selection(
571 DisplayPoint::new(DisplayRow(0), 0),
572 0,
573 gpui::Point::<f32>::default(),
574 window,
575 cx,
576 );
577 });
578
579 assert_eq!(
580 editor
581 .update(cx, |editor, _, cx| display_ranges(editor, cx))
582 .unwrap(),
583 [
584 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
585 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
586 ]
587 );
588
589 _ = editor.update(cx, |editor, window, cx| {
590 editor.end_selection(window, cx);
591 });
592
593 assert_eq!(
594 editor
595 .update(cx, |editor, _, cx| display_ranges(editor, cx))
596 .unwrap(),
597 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
598 );
599}
600
601#[gpui::test]
602fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let editor = cx.add_window(|window, cx| {
606 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
607 build_editor(buffer, window, cx)
608 });
609
610 _ = editor.update(cx, |editor, window, cx| {
611 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
612 });
613
614 _ = editor.update(cx, |editor, window, cx| {
615 editor.end_selection(window, cx);
616 });
617
618 _ = editor.update(cx, |editor, window, cx| {
619 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.end_selection(window, cx);
624 });
625
626 assert_eq!(
627 editor
628 .update(cx, |editor, _, cx| display_ranges(editor, cx))
629 .unwrap(),
630 [
631 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
632 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
633 ]
634 );
635
636 _ = editor.update(cx, |editor, window, cx| {
637 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
638 });
639
640 _ = editor.update(cx, |editor, window, cx| {
641 editor.end_selection(window, cx);
642 });
643
644 assert_eq!(
645 editor
646 .update(cx, |editor, _, cx| display_ranges(editor, cx))
647 .unwrap(),
648 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
649 );
650}
651
652#[gpui::test]
653fn test_canceling_pending_selection(cx: &mut TestAppContext) {
654 init_test(cx, |_| {});
655
656 let editor = cx.add_window(|window, cx| {
657 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
658 build_editor(buffer, window, cx)
659 });
660
661 _ = editor.update(cx, |editor, window, cx| {
662 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
663 assert_eq!(
664 display_ranges(editor, cx),
665 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
666 );
667 });
668
669 _ = editor.update(cx, |editor, window, cx| {
670 editor.update_selection(
671 DisplayPoint::new(DisplayRow(3), 3),
672 0,
673 gpui::Point::<f32>::default(),
674 window,
675 cx,
676 );
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
680 );
681 });
682
683 _ = editor.update(cx, |editor, window, cx| {
684 editor.cancel(&Cancel, window, cx);
685 editor.update_selection(
686 DisplayPoint::new(DisplayRow(1), 1),
687 0,
688 gpui::Point::<f32>::default(),
689 window,
690 cx,
691 );
692 assert_eq!(
693 display_ranges(editor, cx),
694 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
695 );
696 });
697}
698
699#[gpui::test]
700fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
701 init_test(cx, |_| {});
702
703 let editor = cx.add_window(|window, cx| {
704 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
705 build_editor(buffer, window, cx)
706 });
707
708 _ = editor.update(cx, |editor, window, cx| {
709 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
710 assert_eq!(
711 display_ranges(editor, cx),
712 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
713 );
714
715 editor.move_down(&Default::default(), window, cx);
716 assert_eq!(
717 display_ranges(editor, cx),
718 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
719 );
720
721 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
722 assert_eq!(
723 display_ranges(editor, cx),
724 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
725 );
726
727 editor.move_up(&Default::default(), window, cx);
728 assert_eq!(
729 display_ranges(editor, cx),
730 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
731 );
732 });
733}
734
735#[gpui::test]
736fn test_extending_selection(cx: &mut TestAppContext) {
737 init_test(cx, |_| {});
738
739 let editor = cx.add_window(|window, cx| {
740 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
741 build_editor(buffer, window, cx)
742 });
743
744 _ = editor.update(cx, |editor, window, cx| {
745 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
746 editor.end_selection(window, cx);
747 assert_eq!(
748 display_ranges(editor, cx),
749 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
750 );
751
752 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 display_ranges(editor, cx),
756 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
757 );
758
759 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
760 editor.end_selection(window, cx);
761 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
762 assert_eq!(
763 display_ranges(editor, cx),
764 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
765 );
766
767 editor.update_selection(
768 DisplayPoint::new(DisplayRow(0), 1),
769 0,
770 gpui::Point::<f32>::default(),
771 window,
772 cx,
773 );
774 editor.end_selection(window, cx);
775 assert_eq!(
776 display_ranges(editor, cx),
777 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
778 );
779
780 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
781 editor.end_selection(window, cx);
782 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
783 editor.end_selection(window, cx);
784 assert_eq!(
785 display_ranges(editor, cx),
786 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
787 );
788
789 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
790 assert_eq!(
791 display_ranges(editor, cx),
792 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
793 );
794
795 editor.update_selection(
796 DisplayPoint::new(DisplayRow(0), 6),
797 0,
798 gpui::Point::<f32>::default(),
799 window,
800 cx,
801 );
802 assert_eq!(
803 display_ranges(editor, cx),
804 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
805 );
806
807 editor.update_selection(
808 DisplayPoint::new(DisplayRow(0), 1),
809 0,
810 gpui::Point::<f32>::default(),
811 window,
812 cx,
813 );
814 editor.end_selection(window, cx);
815 assert_eq!(
816 display_ranges(editor, cx),
817 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
818 );
819 });
820}
821
822#[gpui::test]
823fn test_clone(cx: &mut TestAppContext) {
824 init_test(cx, |_| {});
825
826 let (text, selection_ranges) = marked_text_ranges(
827 indoc! {"
828 one
829 two
830 threeˇ
831 four
832 fiveˇ
833 "},
834 true,
835 );
836
837 let editor = cx.add_window(|window, cx| {
838 let buffer = MultiBuffer::build_simple(&text, cx);
839 build_editor(buffer, window, cx)
840 });
841
842 _ = editor.update(cx, |editor, window, cx| {
843 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
844 s.select_ranges(
845 selection_ranges
846 .iter()
847 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
848 )
849 });
850 editor.fold_creases(
851 vec![
852 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
853 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
854 ],
855 true,
856 window,
857 cx,
858 );
859 });
860
861 let cloned_editor = editor
862 .update(cx, |editor, _, cx| {
863 cx.open_window(Default::default(), |window, cx| {
864 cx.new(|cx| editor.clone(window, cx))
865 })
866 })
867 .unwrap()
868 .unwrap();
869
870 let snapshot = editor
871 .update(cx, |e, window, cx| e.snapshot(window, cx))
872 .unwrap();
873 let cloned_snapshot = cloned_editor
874 .update(cx, |e, window, cx| e.snapshot(window, cx))
875 .unwrap();
876
877 assert_eq!(
878 cloned_editor
879 .update(cx, |e, _, cx| e.display_text(cx))
880 .unwrap(),
881 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
882 );
883 assert_eq!(
884 cloned_snapshot
885 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
886 .collect::<Vec<_>>(),
887 snapshot
888 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
889 .collect::<Vec<_>>(),
890 );
891 assert_set_eq!(
892 cloned_editor
893 .update(cx, |editor, _, cx| editor
894 .selections
895 .ranges::<Point>(&editor.display_snapshot(cx)))
896 .unwrap(),
897 editor
898 .update(cx, |editor, _, cx| editor
899 .selections
900 .ranges(&editor.display_snapshot(cx)))
901 .unwrap()
902 );
903 assert_set_eq!(
904 cloned_editor
905 .update(cx, |e, _window, cx| e
906 .selections
907 .display_ranges(&e.display_snapshot(cx)))
908 .unwrap(),
909 editor
910 .update(cx, |e, _, cx| e
911 .selections
912 .display_ranges(&e.display_snapshot(cx)))
913 .unwrap()
914 );
915}
916
917#[gpui::test]
918async fn test_navigation_history(cx: &mut TestAppContext) {
919 init_test(cx, |_| {});
920
921 use workspace::item::Item;
922
923 let fs = FakeFs::new(cx.executor());
924 let project = Project::test(fs, [], cx).await;
925 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
926 let workspace = window
927 .read_with(cx, |mw, _| mw.workspace().clone())
928 .unwrap();
929 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
930
931 _ = window.update(cx, |_mw, window, cx| {
932 cx.new(|cx| {
933 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
934 let mut editor = build_editor(buffer, window, cx);
935 let handle = cx.entity();
936 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
937
938 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
939 editor.nav_history.as_mut().unwrap().pop_backward(cx)
940 }
941
942 // Move the cursor a small distance.
943 // Nothing is added to the navigation history.
944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
945 s.select_display_ranges([
946 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
947 ])
948 });
949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
950 s.select_display_ranges([
951 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
952 ])
953 });
954 assert!(pop_history(&mut editor, cx).is_none());
955
956 // Move the cursor a large distance.
957 // The history can jump back to the previous position.
958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
959 s.select_display_ranges([
960 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
961 ])
962 });
963 let nav_entry = pop_history(&mut editor, cx).unwrap();
964 editor.navigate(nav_entry.data.unwrap(), window, cx);
965 assert_eq!(nav_entry.item.id(), cx.entity_id());
966 assert_eq!(
967 editor
968 .selections
969 .display_ranges(&editor.display_snapshot(cx)),
970 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
971 );
972 assert!(pop_history(&mut editor, cx).is_none());
973
974 // Move the cursor a small distance via the mouse.
975 // Nothing is added to the navigation history.
976 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
977 editor.end_selection(window, cx);
978 assert_eq!(
979 editor
980 .selections
981 .display_ranges(&editor.display_snapshot(cx)),
982 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
983 );
984 assert!(pop_history(&mut editor, cx).is_none());
985
986 // Move the cursor a large distance via the mouse.
987 // The history can jump back to the previous position.
988 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
989 editor.end_selection(window, cx);
990 assert_eq!(
991 editor
992 .selections
993 .display_ranges(&editor.display_snapshot(cx)),
994 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
995 );
996 let nav_entry = pop_history(&mut editor, cx).unwrap();
997 editor.navigate(nav_entry.data.unwrap(), window, cx);
998 assert_eq!(nav_entry.item.id(), cx.entity_id());
999 assert_eq!(
1000 editor
1001 .selections
1002 .display_ranges(&editor.display_snapshot(cx)),
1003 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
1004 );
1005 assert!(pop_history(&mut editor, cx).is_none());
1006
1007 // Set scroll position to check later
1008 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
1009 let original_scroll_position = editor
1010 .scroll_manager
1011 .native_anchor(&editor.display_snapshot(cx), cx);
1012
1013 // Jump to the end of the document and adjust scroll
1014 editor.move_to_end(&MoveToEnd, window, cx);
1015 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
1016 assert_ne!(
1017 editor
1018 .scroll_manager
1019 .native_anchor(&editor.display_snapshot(cx), cx),
1020 original_scroll_position
1021 );
1022
1023 let nav_entry = pop_history(&mut editor, cx).unwrap();
1024 editor.navigate(nav_entry.data.unwrap(), window, cx);
1025 assert_eq!(
1026 editor
1027 .scroll_manager
1028 .native_anchor(&editor.display_snapshot(cx), cx),
1029 original_scroll_position
1030 );
1031
1032 let other_buffer =
1033 cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local("test", cx)), cx));
1034
1035 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
1036 let invalid_anchor = other_buffer.update(cx, |buffer, cx| {
1037 buffer.snapshot(cx).anchor_after(MultiBufferOffset(3))
1038 });
1039 let invalid_point = Point::new(9999, 0);
1040 editor.navigate(
1041 Arc::new(NavigationData {
1042 cursor_anchor: invalid_anchor,
1043 cursor_position: invalid_point,
1044 scroll_anchor: ScrollAnchor {
1045 anchor: invalid_anchor,
1046 offset: Default::default(),
1047 },
1048 scroll_top_row: invalid_point.row,
1049 }),
1050 window,
1051 cx,
1052 );
1053 assert_eq!(
1054 editor
1055 .selections
1056 .display_ranges(&editor.display_snapshot(cx)),
1057 &[editor.max_point(cx)..editor.max_point(cx)]
1058 );
1059 assert_eq!(
1060 editor.scroll_position(cx),
1061 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
1062 );
1063
1064 editor
1065 })
1066 });
1067}
1068
1069#[gpui::test]
1070fn test_cancel(cx: &mut TestAppContext) {
1071 init_test(cx, |_| {});
1072
1073 let editor = cx.add_window(|window, cx| {
1074 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
1075 build_editor(buffer, window, cx)
1076 });
1077
1078 _ = editor.update(cx, |editor, window, cx| {
1079 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
1080 editor.update_selection(
1081 DisplayPoint::new(DisplayRow(1), 1),
1082 0,
1083 gpui::Point::<f32>::default(),
1084 window,
1085 cx,
1086 );
1087 editor.end_selection(window, cx);
1088
1089 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1090 editor.update_selection(
1091 DisplayPoint::new(DisplayRow(0), 3),
1092 0,
1093 gpui::Point::<f32>::default(),
1094 window,
1095 cx,
1096 );
1097 editor.end_selection(window, cx);
1098 assert_eq!(
1099 display_ranges(editor, cx),
1100 [
1101 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1102 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1103 ]
1104 );
1105 });
1106
1107 _ = editor.update(cx, |editor, window, cx| {
1108 editor.cancel(&Cancel, window, cx);
1109 assert_eq!(
1110 display_ranges(editor, cx),
1111 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1112 );
1113 });
1114
1115 _ = editor.update(cx, |editor, window, cx| {
1116 editor.cancel(&Cancel, window, cx);
1117 assert_eq!(
1118 display_ranges(editor, cx),
1119 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1120 );
1121 });
1122}
1123
1124#[gpui::test]
1125async fn test_fold_action(cx: &mut TestAppContext) {
1126 init_test(cx, |_| {});
1127
1128 let mut cx = EditorTestContext::new(cx).await;
1129
1130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
1131 cx.set_state(indoc! {"
1132 impl Foo {
1133 // Hello!
1134
1135 fn a() {
1136 1
1137 }
1138
1139 fn b() {
1140 2
1141 }
1142
1143 fn c() {
1144 3
1145 }
1146 }ˇ
1147 "});
1148
1149 cx.update_editor(|editor, window, cx| {
1150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1151 s.select_display_ranges([
1152 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1153 ]);
1154 });
1155 editor.fold(&Fold, window, cx);
1156 assert_eq!(
1157 editor.display_text(cx),
1158 "
1159 impl Foo {
1160 // Hello!
1161
1162 fn a() {
1163 1
1164 }
1165
1166 fn b() {⋯}
1167
1168 fn c() {⋯}
1169 }
1170 "
1171 .unindent(),
1172 );
1173
1174 editor.fold(&Fold, window, cx);
1175 assert_eq!(
1176 editor.display_text(cx),
1177 "
1178 impl Foo {⋯}
1179 "
1180 .unindent(),
1181 );
1182
1183 editor.unfold_lines(&UnfoldLines, window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 impl Foo {
1188 // Hello!
1189
1190 fn a() {
1191 1
1192 }
1193
1194 fn b() {⋯}
1195
1196 fn c() {⋯}
1197 }
1198 "
1199 .unindent(),
1200 );
1201
1202 editor.unfold_lines(&UnfoldLines, window, cx);
1203 assert_eq!(
1204 editor.display_text(cx),
1205 editor.buffer.read(cx).read(cx).text()
1206 );
1207 });
1208}
1209
1210#[gpui::test]
1211fn test_fold_action_without_language(cx: &mut TestAppContext) {
1212 init_test(cx, |_| {});
1213
1214 let editor = cx.add_window(|window, cx| {
1215 let buffer = MultiBuffer::build_simple(
1216 &"
1217 impl Foo {
1218 // Hello!
1219
1220 fn a() {
1221 1
1222 }
1223
1224 fn b() {
1225 2
1226 }
1227
1228 fn c() {
1229 3
1230 }
1231 }
1232 "
1233 .unindent(),
1234 cx,
1235 );
1236 build_editor(buffer, window, cx)
1237 });
1238
1239 _ = editor.update(cx, |editor, window, cx| {
1240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1241 s.select_display_ranges([
1242 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1243 ]);
1244 });
1245 editor.fold(&Fold, window, cx);
1246 assert_eq!(
1247 editor.display_text(cx),
1248 "
1249 impl Foo {
1250 // Hello!
1251
1252 fn a() {
1253 1
1254 }
1255
1256 fn b() {⋯
1257 }
1258
1259 fn c() {⋯
1260 }
1261 }
1262 "
1263 .unindent(),
1264 );
1265
1266 editor.fold(&Fold, window, cx);
1267 assert_eq!(
1268 editor.display_text(cx),
1269 "
1270 impl Foo {⋯
1271 }
1272 "
1273 .unindent(),
1274 );
1275
1276 editor.unfold_lines(&UnfoldLines, window, cx);
1277 assert_eq!(
1278 editor.display_text(cx),
1279 "
1280 impl Foo {
1281 // Hello!
1282
1283 fn a() {
1284 1
1285 }
1286
1287 fn b() {⋯
1288 }
1289
1290 fn c() {⋯
1291 }
1292 }
1293 "
1294 .unindent(),
1295 );
1296
1297 editor.unfold_lines(&UnfoldLines, window, cx);
1298 assert_eq!(
1299 editor.display_text(cx),
1300 editor.buffer.read(cx).read(cx).text()
1301 );
1302 });
1303}
1304
1305#[gpui::test]
1306fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1307 init_test(cx, |_| {});
1308
1309 let editor = cx.add_window(|window, cx| {
1310 let buffer = MultiBuffer::build_simple(
1311 &"
1312 class Foo:
1313 # Hello!
1314
1315 def a():
1316 print(1)
1317
1318 def b():
1319 print(2)
1320
1321 def c():
1322 print(3)
1323 "
1324 .unindent(),
1325 cx,
1326 );
1327 build_editor(buffer, window, cx)
1328 });
1329
1330 _ = editor.update(cx, |editor, window, cx| {
1331 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1332 s.select_display_ranges([
1333 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1334 ]);
1335 });
1336 editor.fold(&Fold, window, cx);
1337 assert_eq!(
1338 editor.display_text(cx),
1339 "
1340 class Foo:
1341 # Hello!
1342
1343 def a():
1344 print(1)
1345
1346 def b():⋯
1347
1348 def c():⋯
1349 "
1350 .unindent(),
1351 );
1352
1353 editor.fold(&Fold, window, cx);
1354 assert_eq!(
1355 editor.display_text(cx),
1356 "
1357 class Foo:⋯
1358 "
1359 .unindent(),
1360 );
1361
1362 editor.unfold_lines(&UnfoldLines, window, cx);
1363 assert_eq!(
1364 editor.display_text(cx),
1365 "
1366 class Foo:
1367 # Hello!
1368
1369 def a():
1370 print(1)
1371
1372 def b():⋯
1373
1374 def c():⋯
1375 "
1376 .unindent(),
1377 );
1378
1379 editor.unfold_lines(&UnfoldLines, window, cx);
1380 assert_eq!(
1381 editor.display_text(cx),
1382 editor.buffer.read(cx).read(cx).text()
1383 );
1384 });
1385}
1386
1387#[gpui::test]
1388fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1389 init_test(cx, |_| {});
1390
1391 let editor = cx.add_window(|window, cx| {
1392 let buffer = MultiBuffer::build_simple(
1393 &"
1394 class Foo:
1395 # Hello!
1396
1397 def a():
1398 print(1)
1399
1400 def b():
1401 print(2)
1402
1403
1404 def c():
1405 print(3)
1406
1407
1408 "
1409 .unindent(),
1410 cx,
1411 );
1412 build_editor(buffer, window, cx)
1413 });
1414
1415 _ = editor.update(cx, |editor, window, cx| {
1416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1417 s.select_display_ranges([
1418 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1419 ]);
1420 });
1421 editor.fold(&Fold, window, cx);
1422 assert_eq!(
1423 editor.display_text(cx),
1424 "
1425 class Foo:
1426 # Hello!
1427
1428 def a():
1429 print(1)
1430
1431 def b():⋯
1432
1433
1434 def c():⋯
1435
1436
1437 "
1438 .unindent(),
1439 );
1440
1441 editor.fold(&Fold, window, cx);
1442 assert_eq!(
1443 editor.display_text(cx),
1444 "
1445 class Foo:⋯
1446
1447
1448 "
1449 .unindent(),
1450 );
1451
1452 editor.unfold_lines(&UnfoldLines, window, cx);
1453 assert_eq!(
1454 editor.display_text(cx),
1455 "
1456 class Foo:
1457 # Hello!
1458
1459 def a():
1460 print(1)
1461
1462 def b():⋯
1463
1464
1465 def c():⋯
1466
1467
1468 "
1469 .unindent(),
1470 );
1471
1472 editor.unfold_lines(&UnfoldLines, window, cx);
1473 assert_eq!(
1474 editor.display_text(cx),
1475 editor.buffer.read(cx).read(cx).text()
1476 );
1477 });
1478}
1479
1480#[gpui::test]
1481async fn test_fold_with_unindented_multiline_raw_string(cx: &mut TestAppContext) {
1482 init_test(cx, |_| {});
1483
1484 let mut cx = EditorTestContext::new(cx).await;
1485
1486 let language = Arc::new(
1487 Language::new(
1488 LanguageConfig::default(),
1489 Some(tree_sitter_rust::LANGUAGE.into()),
1490 )
1491 .with_queries(LanguageQueries {
1492 overrides: Some(Cow::from(indoc! {"
1493 [
1494 (string_literal)
1495 (raw_string_literal)
1496 ] @string
1497 [
1498 (line_comment)
1499 (block_comment)
1500 ] @comment.inclusive
1501 "})),
1502 ..Default::default()
1503 })
1504 .expect("Could not parse queries"),
1505 );
1506
1507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1508 cx.set_state(indoc! {"
1509 fn main() {
1510 let s = r#\"
1511 a
1512 b
1513 c
1514 \"#;
1515 }ˇ
1516 "});
1517
1518 cx.update_editor(|editor, window, cx| {
1519 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1520 assert_eq!(
1521 editor.display_text(cx),
1522 indoc! {"
1523 fn main() {⋯
1524 }
1525 "},
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531async fn test_fold_with_unindented_multiline_raw_string_includes_closing_bracket(
1532 cx: &mut TestAppContext,
1533) {
1534 init_test(cx, |_| {});
1535
1536 let mut cx = EditorTestContext::new(cx).await;
1537
1538 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
1539 cx.set_state(indoc! {"
1540 ˇfn main() {
1541 let s = r#\"
1542 a
1543 b
1544 c
1545 \"#;
1546 }
1547 "});
1548
1549 cx.update_editor(|editor, window, cx| {
1550 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1551 assert_eq!(
1552 editor.display_text(cx),
1553 indoc! {"
1554 fn main() {⋯}
1555 "},
1556 );
1557 });
1558}
1559
1560#[gpui::test]
1561async fn test_fold_with_unindented_multiline_block_comment(cx: &mut TestAppContext) {
1562 init_test(cx, |_| {});
1563
1564 let mut cx = EditorTestContext::new(cx).await;
1565
1566 let language = Arc::new(
1567 Language::new(
1568 LanguageConfig::default(),
1569 Some(tree_sitter_rust::LANGUAGE.into()),
1570 )
1571 .with_queries(LanguageQueries {
1572 overrides: Some(Cow::from(indoc! {"
1573 [
1574 (string_literal)
1575 (raw_string_literal)
1576 ] @string
1577 [
1578 (line_comment)
1579 (block_comment)
1580 ] @comment.inclusive
1581 "})),
1582 ..Default::default()
1583 })
1584 .expect("Could not parse queries"),
1585 );
1586
1587 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1588 cx.set_state(indoc! {"
1589 fn main() {
1590 let x = 1;
1591 /*
1592 unindented comment line
1593 */
1594 }ˇ
1595 "});
1596
1597 cx.update_editor(|editor, window, cx| {
1598 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1599 assert_eq!(
1600 editor.display_text(cx),
1601 indoc! {"
1602 fn main() {⋯
1603 }
1604 "},
1605 );
1606 });
1607}
1608
1609#[gpui::test]
1610async fn test_fold_with_unindented_multiline_block_comment_includes_closing_bracket(
1611 cx: &mut TestAppContext,
1612) {
1613 init_test(cx, |_| {});
1614
1615 let mut cx = EditorTestContext::new(cx).await;
1616
1617 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
1618 cx.set_state(indoc! {"
1619 ˇfn main() {
1620 let x = 1;
1621 /*
1622 unindented comment line
1623 */
1624 }
1625 "});
1626
1627 cx.update_editor(|editor, window, cx| {
1628 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1629 assert_eq!(
1630 editor.display_text(cx),
1631 indoc! {"
1632 fn main() {⋯}
1633 "},
1634 );
1635 });
1636}
1637
1638#[gpui::test]
1639fn test_fold_at_level(cx: &mut TestAppContext) {
1640 init_test(cx, |_| {});
1641
1642 let editor = cx.add_window(|window, cx| {
1643 let buffer = MultiBuffer::build_simple(
1644 &"
1645 class Foo:
1646 # Hello!
1647
1648 def a():
1649 print(1)
1650
1651 def b():
1652 print(2)
1653
1654
1655 class Bar:
1656 # World!
1657
1658 def a():
1659 print(1)
1660
1661 def b():
1662 print(2)
1663
1664
1665 "
1666 .unindent(),
1667 cx,
1668 );
1669 build_editor(buffer, window, cx)
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1674 assert_eq!(
1675 editor.display_text(cx),
1676 "
1677 class Foo:
1678 # Hello!
1679
1680 def a():⋯
1681
1682 def b():⋯
1683
1684
1685 class Bar:
1686 # World!
1687
1688 def a():⋯
1689
1690 def b():⋯
1691
1692
1693 "
1694 .unindent(),
1695 );
1696
1697 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1698 assert_eq!(
1699 editor.display_text(cx),
1700 "
1701 class Foo:⋯
1702
1703
1704 class Bar:⋯
1705
1706
1707 "
1708 .unindent(),
1709 );
1710
1711 editor.unfold_all(&UnfoldAll, window, cx);
1712 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1713 assert_eq!(
1714 editor.display_text(cx),
1715 "
1716 class Foo:
1717 # Hello!
1718
1719 def a():
1720 print(1)
1721
1722 def b():
1723 print(2)
1724
1725
1726 class Bar:
1727 # World!
1728
1729 def a():
1730 print(1)
1731
1732 def b():
1733 print(2)
1734
1735
1736 "
1737 .unindent(),
1738 );
1739
1740 assert_eq!(
1741 editor.display_text(cx),
1742 editor.buffer.read(cx).read(cx).text()
1743 );
1744 let (_, positions) = marked_text_ranges(
1745 &"
1746 class Foo:
1747 # Hello!
1748
1749 def a():
1750 print(1)
1751
1752 def b():
1753 p«riˇ»nt(2)
1754
1755
1756 class Bar:
1757 # World!
1758
1759 def a():
1760 «ˇprint(1)
1761
1762 def b():
1763 print(2)»
1764
1765
1766 "
1767 .unindent(),
1768 true,
1769 );
1770
1771 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1772 s.select_ranges(
1773 positions
1774 .iter()
1775 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1776 )
1777 });
1778
1779 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1780 assert_eq!(
1781 editor.display_text(cx),
1782 "
1783 class Foo:
1784 # Hello!
1785
1786 def a():⋯
1787
1788 def b():
1789 print(2)
1790
1791
1792 class Bar:
1793 # World!
1794
1795 def a():
1796 print(1)
1797
1798 def b():
1799 print(2)
1800
1801
1802 "
1803 .unindent(),
1804 );
1805 });
1806}
1807
1808#[gpui::test]
1809fn test_move_cursor(cx: &mut TestAppContext) {
1810 init_test(cx, |_| {});
1811
1812 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1813 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1814
1815 buffer.update(cx, |buffer, cx| {
1816 buffer.edit(
1817 vec![
1818 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1819 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1820 ],
1821 None,
1822 cx,
1823 );
1824 });
1825 _ = editor.update(cx, |editor, window, cx| {
1826 assert_eq!(
1827 display_ranges(editor, cx),
1828 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1829 );
1830
1831 editor.move_down(&MoveDown, window, cx);
1832 assert_eq!(
1833 display_ranges(editor, cx),
1834 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1835 );
1836
1837 editor.move_right(&MoveRight, window, cx);
1838 assert_eq!(
1839 display_ranges(editor, cx),
1840 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1841 );
1842
1843 editor.move_left(&MoveLeft, window, cx);
1844 assert_eq!(
1845 display_ranges(editor, cx),
1846 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1847 );
1848
1849 editor.move_up(&MoveUp, window, cx);
1850 assert_eq!(
1851 display_ranges(editor, cx),
1852 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1853 );
1854
1855 editor.move_to_end(&MoveToEnd, window, cx);
1856 assert_eq!(
1857 display_ranges(editor, cx),
1858 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1859 );
1860
1861 editor.move_to_beginning(&MoveToBeginning, window, cx);
1862 assert_eq!(
1863 display_ranges(editor, cx),
1864 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1865 );
1866
1867 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1868 s.select_display_ranges([
1869 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1870 ]);
1871 });
1872 editor.select_to_beginning(&SelectToBeginning, window, cx);
1873 assert_eq!(
1874 display_ranges(editor, cx),
1875 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1876 );
1877
1878 editor.select_to_end(&SelectToEnd, window, cx);
1879 assert_eq!(
1880 display_ranges(editor, cx),
1881 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1882 );
1883 });
1884}
1885
1886#[gpui::test]
1887fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1888 init_test(cx, |_| {});
1889
1890 let editor = cx.add_window(|window, cx| {
1891 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1892 build_editor(buffer, window, cx)
1893 });
1894
1895 assert_eq!('🟥'.len_utf8(), 4);
1896 assert_eq!('α'.len_utf8(), 2);
1897
1898 _ = editor.update(cx, |editor, window, cx| {
1899 editor.fold_creases(
1900 vec![
1901 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1902 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1903 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1904 ],
1905 true,
1906 window,
1907 cx,
1908 );
1909 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1910
1911 editor.move_right(&MoveRight, window, cx);
1912 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1913 editor.move_right(&MoveRight, window, cx);
1914 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1915 editor.move_right(&MoveRight, window, cx);
1916 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1917
1918 editor.move_down(&MoveDown, window, cx);
1919 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1920 editor.move_left(&MoveLeft, window, cx);
1921 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1922 editor.move_left(&MoveLeft, window, cx);
1923 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1924 editor.move_left(&MoveLeft, window, cx);
1925 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1926
1927 editor.move_down(&MoveDown, window, cx);
1928 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1929 editor.move_right(&MoveRight, window, cx);
1930 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1931 editor.move_right(&MoveRight, window, cx);
1932 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1933 editor.move_right(&MoveRight, window, cx);
1934 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1935
1936 editor.move_up(&MoveUp, window, cx);
1937 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1938 editor.move_down(&MoveDown, window, cx);
1939 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1940 editor.move_up(&MoveUp, window, cx);
1941 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1942
1943 editor.move_up(&MoveUp, window, cx);
1944 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1945 editor.move_left(&MoveLeft, window, cx);
1946 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1947 editor.move_left(&MoveLeft, window, cx);
1948 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1949 });
1950}
1951
1952#[gpui::test]
1953fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1954 init_test(cx, |_| {});
1955
1956 let editor = cx.add_window(|window, cx| {
1957 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1958 build_editor(buffer, window, cx)
1959 });
1960 _ = editor.update(cx, |editor, window, cx| {
1961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1962 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1963 });
1964
1965 // moving above start of document should move selection to start of document,
1966 // but the next move down should still be at the original goal_x
1967 editor.move_up(&MoveUp, window, cx);
1968 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1969
1970 editor.move_down(&MoveDown, window, cx);
1971 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1972
1973 editor.move_down(&MoveDown, window, cx);
1974 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1975
1976 editor.move_down(&MoveDown, window, cx);
1977 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1978
1979 editor.move_down(&MoveDown, window, cx);
1980 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1981
1982 // moving past end of document should not change goal_x
1983 editor.move_down(&MoveDown, window, cx);
1984 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1985
1986 editor.move_down(&MoveDown, window, cx);
1987 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1988
1989 editor.move_up(&MoveUp, window, cx);
1990 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1991
1992 editor.move_up(&MoveUp, window, cx);
1993 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1994
1995 editor.move_up(&MoveUp, window, cx);
1996 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1997 });
1998}
1999
2000#[gpui::test]
2001fn test_beginning_end_of_line(cx: &mut TestAppContext) {
2002 init_test(cx, |_| {});
2003 let move_to_beg = MoveToBeginningOfLine {
2004 stop_at_soft_wraps: true,
2005 stop_at_indent: true,
2006 };
2007
2008 let delete_to_beg = DeleteToBeginningOfLine {
2009 stop_at_indent: false,
2010 };
2011
2012 let move_to_end = MoveToEndOfLine {
2013 stop_at_soft_wraps: true,
2014 };
2015
2016 let editor = cx.add_window(|window, cx| {
2017 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2018 build_editor(buffer, window, cx)
2019 });
2020 _ = editor.update(cx, |editor, window, cx| {
2021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2022 s.select_display_ranges([
2023 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2024 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2025 ]);
2026 });
2027 });
2028
2029 _ = editor.update(cx, |editor, window, cx| {
2030 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2031 assert_eq!(
2032 display_ranges(editor, cx),
2033 &[
2034 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2035 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2036 ]
2037 );
2038 });
2039
2040 _ = editor.update(cx, |editor, window, cx| {
2041 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2042 assert_eq!(
2043 display_ranges(editor, cx),
2044 &[
2045 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2046 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2047 ]
2048 );
2049 });
2050
2051 _ = editor.update(cx, |editor, window, cx| {
2052 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2053 assert_eq!(
2054 display_ranges(editor, cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060 });
2061
2062 _ = editor.update(cx, |editor, window, cx| {
2063 editor.move_to_end_of_line(&move_to_end, window, cx);
2064 assert_eq!(
2065 display_ranges(editor, cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2068 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
2069 ]
2070 );
2071 });
2072
2073 // Moving to the end of line again is a no-op.
2074 _ = editor.update(cx, |editor, window, cx| {
2075 editor.move_to_end_of_line(&move_to_end, window, cx);
2076 assert_eq!(
2077 display_ranges(editor, cx),
2078 &[
2079 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2080 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
2081 ]
2082 );
2083 });
2084
2085 _ = editor.update(cx, |editor, window, cx| {
2086 editor.move_left(&MoveLeft, window, cx);
2087 editor.select_to_beginning_of_line(
2088 &SelectToBeginningOfLine {
2089 stop_at_soft_wraps: true,
2090 stop_at_indent: true,
2091 },
2092 window,
2093 cx,
2094 );
2095 assert_eq!(
2096 display_ranges(editor, cx),
2097 &[
2098 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2099 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2100 ]
2101 );
2102 });
2103
2104 _ = editor.update(cx, |editor, window, cx| {
2105 editor.select_to_beginning_of_line(
2106 &SelectToBeginningOfLine {
2107 stop_at_soft_wraps: true,
2108 stop_at_indent: true,
2109 },
2110 window,
2111 cx,
2112 );
2113 assert_eq!(
2114 display_ranges(editor, cx),
2115 &[
2116 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2117 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2118 ]
2119 );
2120 });
2121
2122 _ = editor.update(cx, |editor, window, cx| {
2123 editor.select_to_beginning_of_line(
2124 &SelectToBeginningOfLine {
2125 stop_at_soft_wraps: true,
2126 stop_at_indent: true,
2127 },
2128 window,
2129 cx,
2130 );
2131 assert_eq!(
2132 display_ranges(editor, cx),
2133 &[
2134 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2135 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2136 ]
2137 );
2138 });
2139
2140 _ = editor.update(cx, |editor, window, cx| {
2141 editor.select_to_end_of_line(
2142 &SelectToEndOfLine {
2143 stop_at_soft_wraps: true,
2144 },
2145 window,
2146 cx,
2147 );
2148 assert_eq!(
2149 display_ranges(editor, cx),
2150 &[
2151 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
2152 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
2153 ]
2154 );
2155 });
2156
2157 _ = editor.update(cx, |editor, window, cx| {
2158 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
2159 assert_eq!(editor.display_text(cx), "ab\n de");
2160 assert_eq!(
2161 display_ranges(editor, cx),
2162 &[
2163 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2164 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2165 ]
2166 );
2167 });
2168
2169 _ = editor.update(cx, |editor, window, cx| {
2170 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2171 assert_eq!(editor.display_text(cx), "\n");
2172 assert_eq!(
2173 display_ranges(editor, cx),
2174 &[
2175 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2176 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2177 ]
2178 );
2179 });
2180}
2181
2182#[gpui::test]
2183fn test_beginning_of_line_single_line_editor(cx: &mut TestAppContext) {
2184 init_test(cx, |_| {});
2185
2186 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
2187
2188 _ = editor.update(cx, |editor, window, cx| {
2189 editor.set_text(" indented text", window, cx);
2190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2191 s.select_display_ranges([
2192 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 10)
2193 ]);
2194 });
2195
2196 editor.move_to_beginning_of_line(
2197 &MoveToBeginningOfLine {
2198 stop_at_soft_wraps: true,
2199 stop_at_indent: true,
2200 },
2201 window,
2202 cx,
2203 );
2204 assert_eq!(
2205 display_ranges(editor, cx),
2206 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2207 );
2208 });
2209
2210 _ = editor.update(cx, |editor, window, cx| {
2211 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2212 s.select_display_ranges([
2213 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 10)
2214 ]);
2215 });
2216
2217 editor.select_to_beginning_of_line(
2218 &SelectToBeginningOfLine {
2219 stop_at_soft_wraps: true,
2220 stop_at_indent: true,
2221 },
2222 window,
2223 cx,
2224 );
2225 assert_eq!(
2226 display_ranges(editor, cx),
2227 &[DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 0)]
2228 );
2229 });
2230}
2231
2232#[gpui::test]
2233fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
2234 init_test(cx, |_| {});
2235 let move_to_beg = MoveToBeginningOfLine {
2236 stop_at_soft_wraps: false,
2237 stop_at_indent: false,
2238 };
2239
2240 let move_to_end = MoveToEndOfLine {
2241 stop_at_soft_wraps: false,
2242 };
2243
2244 let editor = cx.add_window(|window, cx| {
2245 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
2246 build_editor(buffer, window, cx)
2247 });
2248
2249 _ = editor.update(cx, |editor, window, cx| {
2250 editor.set_wrap_width(Some(140.0.into()), cx);
2251
2252 // We expect the following lines after wrapping
2253 // ```
2254 // thequickbrownfox
2255 // jumpedoverthelazydo
2256 // gs
2257 // ```
2258 // The final `gs` was soft-wrapped onto a new line.
2259 assert_eq!(
2260 "thequickbrownfox\njumpedoverthelaz\nydogs",
2261 editor.display_text(cx),
2262 );
2263
2264 // First, let's assert behavior on the first line, that was not soft-wrapped.
2265 // Start the cursor at the `k` on the first line
2266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2267 s.select_display_ranges([
2268 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
2269 ]);
2270 });
2271
2272 // Moving to the beginning of the line should put us at the beginning of the line.
2273 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2274 assert_eq!(
2275 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
2276 display_ranges(editor, cx)
2277 );
2278
2279 // Moving to the end of the line should put us at the end of the line.
2280 editor.move_to_end_of_line(&move_to_end, window, cx);
2281 assert_eq!(
2282 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
2283 display_ranges(editor, cx)
2284 );
2285
2286 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
2287 // Start the cursor at the last line (`y` that was wrapped to a new line)
2288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2289 s.select_display_ranges([
2290 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
2291 ]);
2292 });
2293
2294 // Moving to the beginning of the line should put us at the start of the second line of
2295 // display text, i.e., the `j`.
2296 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2297 assert_eq!(
2298 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
2299 display_ranges(editor, cx)
2300 );
2301
2302 // Moving to the beginning of the line again should be a no-op.
2303 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2304 assert_eq!(
2305 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
2306 display_ranges(editor, cx)
2307 );
2308
2309 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
2310 // next display line.
2311 editor.move_to_end_of_line(&move_to_end, window, cx);
2312 assert_eq!(
2313 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
2314 display_ranges(editor, cx)
2315 );
2316
2317 // Moving to the end of the line again should be a no-op.
2318 editor.move_to_end_of_line(&move_to_end, window, cx);
2319 assert_eq!(
2320 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
2321 display_ranges(editor, cx)
2322 );
2323 });
2324}
2325
2326#[gpui::test]
2327fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
2328 init_test(cx, |_| {});
2329
2330 let move_to_beg = MoveToBeginningOfLine {
2331 stop_at_soft_wraps: true,
2332 stop_at_indent: true,
2333 };
2334
2335 let select_to_beg = SelectToBeginningOfLine {
2336 stop_at_soft_wraps: true,
2337 stop_at_indent: true,
2338 };
2339
2340 let delete_to_beg = DeleteToBeginningOfLine {
2341 stop_at_indent: true,
2342 };
2343
2344 let move_to_end = MoveToEndOfLine {
2345 stop_at_soft_wraps: false,
2346 };
2347
2348 let editor = cx.add_window(|window, cx| {
2349 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2350 build_editor(buffer, window, cx)
2351 });
2352
2353 _ = editor.update(cx, |editor, window, cx| {
2354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2355 s.select_display_ranges([
2356 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2357 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2358 ]);
2359 });
2360
2361 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2362 // and the second cursor at the first non-whitespace character in the line.
2363 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2364 assert_eq!(
2365 display_ranges(editor, cx),
2366 &[
2367 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2368 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2369 ]
2370 );
2371
2372 // Moving to the beginning of the line again should be a no-op for the first cursor,
2373 // and should move the second cursor to the beginning of the line.
2374 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2375 assert_eq!(
2376 display_ranges(editor, cx),
2377 &[
2378 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2379 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2380 ]
2381 );
2382
2383 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2384 // and should move the second cursor back to the first non-whitespace character in the line.
2385 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2386 assert_eq!(
2387 display_ranges(editor, cx),
2388 &[
2389 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2390 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2391 ]
2392 );
2393
2394 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2395 // and to the first non-whitespace character in the line for the second cursor.
2396 editor.move_to_end_of_line(&move_to_end, window, cx);
2397 editor.move_left(&MoveLeft, window, cx);
2398 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2399 assert_eq!(
2400 display_ranges(editor, cx),
2401 &[
2402 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2403 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2404 ]
2405 );
2406
2407 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2408 // and should select to the beginning of the line for the second cursor.
2409 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2410 assert_eq!(
2411 display_ranges(editor, cx),
2412 &[
2413 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2414 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2415 ]
2416 );
2417
2418 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2419 // and should delete to the first non-whitespace character in the line for the second cursor.
2420 editor.move_to_end_of_line(&move_to_end, window, cx);
2421 editor.move_left(&MoveLeft, window, cx);
2422 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2423 assert_eq!(editor.text(cx), "c\n f");
2424 });
2425}
2426
2427#[gpui::test]
2428fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2429 init_test(cx, |_| {});
2430
2431 let move_to_beg = MoveToBeginningOfLine {
2432 stop_at_soft_wraps: true,
2433 stop_at_indent: true,
2434 };
2435
2436 let editor = cx.add_window(|window, cx| {
2437 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2438 build_editor(buffer, window, cx)
2439 });
2440
2441 _ = editor.update(cx, |editor, window, cx| {
2442 // test cursor between line_start and indent_start
2443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2444 s.select_display_ranges([
2445 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2446 ]);
2447 });
2448
2449 // cursor should move to line_start
2450 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2451 assert_eq!(
2452 display_ranges(editor, cx),
2453 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2454 );
2455
2456 // cursor should move to indent_start
2457 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2458 assert_eq!(
2459 display_ranges(editor, cx),
2460 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2461 );
2462
2463 // cursor should move to back to line_start
2464 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2465 assert_eq!(
2466 display_ranges(editor, cx),
2467 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2468 );
2469 });
2470}
2471
2472#[gpui::test]
2473fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2474 init_test(cx, |_| {});
2475
2476 let editor = cx.add_window(|window, cx| {
2477 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2478 build_editor(buffer, window, cx)
2479 });
2480 _ = editor.update(cx, |editor, window, cx| {
2481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2482 s.select_display_ranges([
2483 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2484 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2485 ])
2486 });
2487 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2488 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2489
2490 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2491 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2492
2493 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2494 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2495
2496 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2497 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2498
2499 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2500 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2501
2502 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2503 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2504
2505 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2506 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2507
2508 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2509 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2510
2511 editor.move_right(&MoveRight, window, cx);
2512 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2513 assert_selection_ranges(
2514 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2515 editor,
2516 cx,
2517 );
2518
2519 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2520 assert_selection_ranges(
2521 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2522 editor,
2523 cx,
2524 );
2525
2526 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2527 assert_selection_ranges(
2528 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2529 editor,
2530 cx,
2531 );
2532 });
2533}
2534
2535#[gpui::test]
2536fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2537 init_test(cx, |_| {});
2538
2539 let editor = cx.add_window(|window, cx| {
2540 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2541 build_editor(buffer, window, cx)
2542 });
2543
2544 _ = editor.update(cx, |editor, window, cx| {
2545 editor.set_wrap_width(Some(140.0.into()), cx);
2546 assert_eq!(
2547 editor.display_text(cx),
2548 "use one::{\n two::three::\n four::five\n};"
2549 );
2550
2551 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2552 s.select_display_ranges([
2553 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2554 ]);
2555 });
2556
2557 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2558 assert_eq!(
2559 display_ranges(editor, cx),
2560 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2561 );
2562
2563 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2564 assert_eq!(
2565 display_ranges(editor, cx),
2566 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2567 );
2568
2569 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2570 assert_eq!(
2571 display_ranges(editor, cx),
2572 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2573 );
2574
2575 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2576 assert_eq!(
2577 display_ranges(editor, cx),
2578 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2579 );
2580
2581 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2582 assert_eq!(
2583 display_ranges(editor, cx),
2584 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2585 );
2586
2587 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2588 assert_eq!(
2589 display_ranges(editor, cx),
2590 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2591 );
2592 });
2593}
2594
2595#[gpui::test]
2596async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2597 init_test(cx, |_| {});
2598 let mut cx = EditorTestContext::new(cx).await;
2599
2600 let line_height = cx.update_editor(|editor, window, cx| {
2601 editor
2602 .style(cx)
2603 .text
2604 .line_height_in_pixels(window.rem_size())
2605 });
2606 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2607
2608 // The third line only contains a single space so we can later assert that the
2609 // editor's paragraph movement considers a non-blank line as a paragraph
2610 // boundary.
2611 cx.set_state(&"ˇone\ntwo\n \nthree\nfourˇ\nfive\n\nsix");
2612
2613 cx.update_editor(|editor, window, cx| {
2614 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2615 });
2616 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\nˇ\nsix");
2617
2618 cx.update_editor(|editor, window, cx| {
2619 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2620 });
2621 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsixˇ");
2622
2623 cx.update_editor(|editor, window, cx| {
2624 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2625 });
2626 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\n\nsixˇ");
2627
2628 cx.update_editor(|editor, window, cx| {
2629 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2630 });
2631 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsix");
2632
2633 cx.update_editor(|editor, window, cx| {
2634 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2635 });
2636
2637 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\n\nsix");
2638
2639 cx.update_editor(|editor, window, cx| {
2640 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2641 });
2642 cx.assert_editor_state(&"ˇone\ntwo\n \nthree\nfour\nfive\n\nsix");
2643}
2644
2645#[gpui::test]
2646async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2647 init_test(cx, |_| {});
2648 let mut cx = EditorTestContext::new(cx).await;
2649 let line_height = cx.update_editor(|editor, window, cx| {
2650 editor
2651 .style(cx)
2652 .text
2653 .line_height_in_pixels(window.rem_size())
2654 });
2655 let window = cx.window;
2656 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2657
2658 cx.set_state(
2659 r#"ˇone
2660 two
2661 three
2662 four
2663 five
2664 six
2665 seven
2666 eight
2667 nine
2668 ten
2669 "#,
2670 );
2671
2672 cx.update_editor(|editor, window, cx| {
2673 assert_eq!(
2674 editor.snapshot(window, cx).scroll_position(),
2675 gpui::Point::new(0., 0.)
2676 );
2677 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2678 assert_eq!(
2679 editor.snapshot(window, cx).scroll_position(),
2680 gpui::Point::new(0., 3.)
2681 );
2682 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2683 assert_eq!(
2684 editor.snapshot(window, cx).scroll_position(),
2685 gpui::Point::new(0., 6.)
2686 );
2687 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2688 assert_eq!(
2689 editor.snapshot(window, cx).scroll_position(),
2690 gpui::Point::new(0., 3.)
2691 );
2692
2693 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2694 assert_eq!(
2695 editor.snapshot(window, cx).scroll_position(),
2696 gpui::Point::new(0., 1.)
2697 );
2698 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2699 assert_eq!(
2700 editor.snapshot(window, cx).scroll_position(),
2701 gpui::Point::new(0., 3.)
2702 );
2703 });
2704}
2705
2706#[gpui::test]
2707async fn test_autoscroll(cx: &mut TestAppContext) {
2708 init_test(cx, |_| {});
2709 let mut cx = EditorTestContext::new(cx).await;
2710
2711 let line_height = cx.update_editor(|editor, window, cx| {
2712 editor.set_vertical_scroll_margin(2, cx);
2713 editor
2714 .style(cx)
2715 .text
2716 .line_height_in_pixels(window.rem_size())
2717 });
2718 let window = cx.window;
2719 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2720
2721 cx.set_state(
2722 r#"ˇone
2723 two
2724 three
2725 four
2726 five
2727 six
2728 seven
2729 eight
2730 nine
2731 ten
2732 "#,
2733 );
2734 cx.update_editor(|editor, window, cx| {
2735 assert_eq!(
2736 editor.snapshot(window, cx).scroll_position(),
2737 gpui::Point::new(0., 0.0)
2738 );
2739 });
2740
2741 // Add a cursor below the visible area. Since both cursors cannot fit
2742 // on screen, the editor autoscrolls to reveal the newest cursor, and
2743 // allows the vertical scroll margin below that cursor.
2744 cx.update_editor(|editor, window, cx| {
2745 editor.change_selections(Default::default(), window, cx, |selections| {
2746 selections.select_ranges([
2747 Point::new(0, 0)..Point::new(0, 0),
2748 Point::new(6, 0)..Point::new(6, 0),
2749 ]);
2750 })
2751 });
2752 cx.update_editor(|editor, window, cx| {
2753 assert_eq!(
2754 editor.snapshot(window, cx).scroll_position(),
2755 gpui::Point::new(0., 3.0)
2756 );
2757 });
2758
2759 // Move down. The editor cursor scrolls down to track the newest cursor.
2760 cx.update_editor(|editor, window, cx| {
2761 editor.move_down(&Default::default(), window, cx);
2762 });
2763 cx.update_editor(|editor, window, cx| {
2764 assert_eq!(
2765 editor.snapshot(window, cx).scroll_position(),
2766 gpui::Point::new(0., 4.0)
2767 );
2768 });
2769
2770 // Add a cursor above the visible area. Since both cursors fit on screen,
2771 // the editor scrolls to show both.
2772 cx.update_editor(|editor, window, cx| {
2773 editor.change_selections(Default::default(), window, cx, |selections| {
2774 selections.select_ranges([
2775 Point::new(1, 0)..Point::new(1, 0),
2776 Point::new(6, 0)..Point::new(6, 0),
2777 ]);
2778 })
2779 });
2780 cx.update_editor(|editor, window, cx| {
2781 assert_eq!(
2782 editor.snapshot(window, cx).scroll_position(),
2783 gpui::Point::new(0., 1.0)
2784 );
2785 });
2786}
2787
2788#[gpui::test]
2789async fn test_exclude_overscroll_margin_clamps_scroll_position(cx: &mut TestAppContext) {
2790 init_test(cx, |_| {});
2791 update_test_editor_settings(cx, &|settings| {
2792 settings.scroll_beyond_last_line = Some(ScrollBeyondLastLine::OnePage);
2793 });
2794
2795 let mut cx = EditorTestContext::new(cx).await;
2796
2797 let line_height = cx.update_editor(|editor, window, cx| {
2798 editor.set_mode(EditorMode::Full {
2799 scale_ui_elements_with_buffer_font_size: false,
2800 show_active_line_background: false,
2801 sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
2802 });
2803 editor
2804 .style(cx)
2805 .text
2806 .line_height_in_pixels(window.rem_size())
2807 });
2808 let window = cx.window;
2809 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2810 cx.set_state(
2811 &r#"
2812 ˇone
2813 two
2814 three
2815 four
2816 five
2817 six
2818 seven
2819 eight
2820 nine
2821 ten
2822 eleven
2823 "#
2824 .unindent(),
2825 );
2826
2827 cx.update_editor(|editor, window, cx| {
2828 let snapshot = editor.snapshot(window, cx);
2829 let max_scroll_top =
2830 (snapshot.max_point().row().as_f64() - editor.visible_line_count().unwrap() + 1.)
2831 .max(0.);
2832
2833 editor.set_scroll_position(gpui::Point::new(0., max_scroll_top + 10.), window, cx);
2834
2835 assert_eq!(
2836 editor.snapshot(window, cx).scroll_position(),
2837 gpui::Point::new(0., max_scroll_top)
2838 );
2839 });
2840}
2841
2842#[gpui::test]
2843async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2844 init_test(cx, |_| {});
2845 let mut cx = EditorTestContext::new(cx).await;
2846
2847 let line_height = cx.update_editor(|editor, window, cx| {
2848 editor
2849 .style(cx)
2850 .text
2851 .line_height_in_pixels(window.rem_size())
2852 });
2853 let window = cx.window;
2854 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2855 cx.set_state(
2856 &r#"
2857 ˇone
2858 two
2859 threeˇ
2860 four
2861 five
2862 six
2863 seven
2864 eight
2865 nine
2866 ten
2867 "#
2868 .unindent(),
2869 );
2870
2871 cx.update_editor(|editor, window, cx| {
2872 editor.move_page_down(&MovePageDown::default(), window, cx)
2873 });
2874 cx.assert_editor_state(
2875 &r#"
2876 one
2877 two
2878 three
2879 ˇfour
2880 five
2881 sixˇ
2882 seven
2883 eight
2884 nine
2885 ten
2886 "#
2887 .unindent(),
2888 );
2889
2890 cx.update_editor(|editor, window, cx| {
2891 editor.move_page_down(&MovePageDown::default(), window, cx)
2892 });
2893 cx.assert_editor_state(
2894 &r#"
2895 one
2896 two
2897 three
2898 four
2899 five
2900 six
2901 ˇseven
2902 eight
2903 nineˇ
2904 ten
2905 "#
2906 .unindent(),
2907 );
2908
2909 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2910 cx.assert_editor_state(
2911 &r#"
2912 one
2913 two
2914 three
2915 ˇfour
2916 five
2917 sixˇ
2918 seven
2919 eight
2920 nine
2921 ten
2922 "#
2923 .unindent(),
2924 );
2925
2926 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2927 cx.assert_editor_state(
2928 &r#"
2929 ˇone
2930 two
2931 threeˇ
2932 four
2933 five
2934 six
2935 seven
2936 eight
2937 nine
2938 ten
2939 "#
2940 .unindent(),
2941 );
2942
2943 // Test select collapsing
2944 cx.update_editor(|editor, window, cx| {
2945 editor.move_page_down(&MovePageDown::default(), window, cx);
2946 editor.move_page_down(&MovePageDown::default(), window, cx);
2947 editor.move_page_down(&MovePageDown::default(), window, cx);
2948 });
2949 cx.assert_editor_state(
2950 &r#"
2951 one
2952 two
2953 three
2954 four
2955 five
2956 six
2957 seven
2958 eight
2959 nine
2960 ˇten
2961 ˇ"#
2962 .unindent(),
2963 );
2964}
2965
2966#[gpui::test]
2967async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2968 init_test(cx, |_| {});
2969 let mut cx = EditorTestContext::new(cx).await;
2970 cx.set_state("one «two threeˇ» four");
2971 cx.update_editor(|editor, window, cx| {
2972 editor.delete_to_beginning_of_line(
2973 &DeleteToBeginningOfLine {
2974 stop_at_indent: false,
2975 },
2976 window,
2977 cx,
2978 );
2979 assert_eq!(editor.text(cx), " four");
2980 });
2981}
2982
2983#[gpui::test]
2984async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2985 init_test(cx, |_| {});
2986
2987 let mut cx = EditorTestContext::new(cx).await;
2988
2989 // For an empty selection, the preceding word fragment is deleted.
2990 // For non-empty selections, only selected characters are deleted.
2991 cx.set_state("onˇe two t«hreˇ»e four");
2992 cx.update_editor(|editor, window, cx| {
2993 editor.delete_to_previous_word_start(
2994 &DeleteToPreviousWordStart {
2995 ignore_newlines: false,
2996 ignore_brackets: false,
2997 },
2998 window,
2999 cx,
3000 );
3001 });
3002 cx.assert_editor_state("ˇe two tˇe four");
3003
3004 cx.set_state("e tˇwo te «fˇ»our");
3005 cx.update_editor(|editor, window, cx| {
3006 editor.delete_to_next_word_end(
3007 &DeleteToNextWordEnd {
3008 ignore_newlines: false,
3009 ignore_brackets: false,
3010 },
3011 window,
3012 cx,
3013 );
3014 });
3015 cx.assert_editor_state("e tˇ te ˇour");
3016}
3017
3018#[gpui::test]
3019async fn test_delete_whitespaces(cx: &mut TestAppContext) {
3020 init_test(cx, |_| {});
3021
3022 let mut cx = EditorTestContext::new(cx).await;
3023
3024 cx.set_state("here is some text ˇwith a space");
3025 cx.update_editor(|editor, window, cx| {
3026 editor.delete_to_previous_word_start(
3027 &DeleteToPreviousWordStart {
3028 ignore_newlines: false,
3029 ignore_brackets: true,
3030 },
3031 window,
3032 cx,
3033 );
3034 });
3035 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
3036 cx.assert_editor_state("here is some textˇwith a space");
3037
3038 cx.set_state("here is some text ˇwith a space");
3039 cx.update_editor(|editor, window, cx| {
3040 editor.delete_to_previous_word_start(
3041 &DeleteToPreviousWordStart {
3042 ignore_newlines: false,
3043 ignore_brackets: false,
3044 },
3045 window,
3046 cx,
3047 );
3048 });
3049 cx.assert_editor_state("here is some textˇwith a space");
3050
3051 cx.set_state("here is some textˇ with a space");
3052 cx.update_editor(|editor, window, cx| {
3053 editor.delete_to_next_word_end(
3054 &DeleteToNextWordEnd {
3055 ignore_newlines: false,
3056 ignore_brackets: true,
3057 },
3058 window,
3059 cx,
3060 );
3061 });
3062 // Same happens in the other direction.
3063 cx.assert_editor_state("here is some textˇwith a space");
3064
3065 cx.set_state("here is some textˇ with a space");
3066 cx.update_editor(|editor, window, cx| {
3067 editor.delete_to_next_word_end(
3068 &DeleteToNextWordEnd {
3069 ignore_newlines: false,
3070 ignore_brackets: false,
3071 },
3072 window,
3073 cx,
3074 );
3075 });
3076 cx.assert_editor_state("here is some textˇwith a space");
3077
3078 cx.set_state("here is some textˇ with a space");
3079 cx.update_editor(|editor, window, cx| {
3080 editor.delete_to_next_word_end(
3081 &DeleteToNextWordEnd {
3082 ignore_newlines: true,
3083 ignore_brackets: false,
3084 },
3085 window,
3086 cx,
3087 );
3088 });
3089 cx.assert_editor_state("here is some textˇwith a space");
3090 cx.update_editor(|editor, window, cx| {
3091 editor.delete_to_previous_word_start(
3092 &DeleteToPreviousWordStart {
3093 ignore_newlines: true,
3094 ignore_brackets: false,
3095 },
3096 window,
3097 cx,
3098 );
3099 });
3100 cx.assert_editor_state("here is some ˇwith a space");
3101 cx.update_editor(|editor, window, cx| {
3102 editor.delete_to_previous_word_start(
3103 &DeleteToPreviousWordStart {
3104 ignore_newlines: true,
3105 ignore_brackets: false,
3106 },
3107 window,
3108 cx,
3109 );
3110 });
3111 // Single whitespaces are removed with the word behind them.
3112 cx.assert_editor_state("here is ˇwith a space");
3113 cx.update_editor(|editor, window, cx| {
3114 editor.delete_to_previous_word_start(
3115 &DeleteToPreviousWordStart {
3116 ignore_newlines: true,
3117 ignore_brackets: false,
3118 },
3119 window,
3120 cx,
3121 );
3122 });
3123 cx.assert_editor_state("here ˇwith a space");
3124 cx.update_editor(|editor, window, cx| {
3125 editor.delete_to_previous_word_start(
3126 &DeleteToPreviousWordStart {
3127 ignore_newlines: true,
3128 ignore_brackets: false,
3129 },
3130 window,
3131 cx,
3132 );
3133 });
3134 cx.assert_editor_state("ˇwith a space");
3135 cx.update_editor(|editor, window, cx| {
3136 editor.delete_to_previous_word_start(
3137 &DeleteToPreviousWordStart {
3138 ignore_newlines: true,
3139 ignore_brackets: false,
3140 },
3141 window,
3142 cx,
3143 );
3144 });
3145 cx.assert_editor_state("ˇwith a space");
3146 cx.update_editor(|editor, window, cx| {
3147 editor.delete_to_next_word_end(
3148 &DeleteToNextWordEnd {
3149 ignore_newlines: true,
3150 ignore_brackets: false,
3151 },
3152 window,
3153 cx,
3154 );
3155 });
3156 // Same happens in the other direction.
3157 cx.assert_editor_state("ˇ a space");
3158 cx.update_editor(|editor, window, cx| {
3159 editor.delete_to_next_word_end(
3160 &DeleteToNextWordEnd {
3161 ignore_newlines: true,
3162 ignore_brackets: false,
3163 },
3164 window,
3165 cx,
3166 );
3167 });
3168 cx.assert_editor_state("ˇ space");
3169 cx.update_editor(|editor, window, cx| {
3170 editor.delete_to_next_word_end(
3171 &DeleteToNextWordEnd {
3172 ignore_newlines: true,
3173 ignore_brackets: false,
3174 },
3175 window,
3176 cx,
3177 );
3178 });
3179 cx.assert_editor_state("ˇ");
3180 cx.update_editor(|editor, window, cx| {
3181 editor.delete_to_next_word_end(
3182 &DeleteToNextWordEnd {
3183 ignore_newlines: true,
3184 ignore_brackets: false,
3185 },
3186 window,
3187 cx,
3188 );
3189 });
3190 cx.assert_editor_state("ˇ");
3191 cx.update_editor(|editor, window, cx| {
3192 editor.delete_to_previous_word_start(
3193 &DeleteToPreviousWordStart {
3194 ignore_newlines: true,
3195 ignore_brackets: false,
3196 },
3197 window,
3198 cx,
3199 );
3200 });
3201 cx.assert_editor_state("ˇ");
3202}
3203
3204#[gpui::test]
3205async fn test_delete_to_bracket(cx: &mut TestAppContext) {
3206 init_test(cx, |_| {});
3207
3208 let language = Arc::new(
3209 Language::new(
3210 LanguageConfig {
3211 brackets: BracketPairConfig {
3212 pairs: vec![
3213 BracketPair {
3214 start: "\"".to_string(),
3215 end: "\"".to_string(),
3216 close: true,
3217 surround: true,
3218 newline: false,
3219 },
3220 BracketPair {
3221 start: "(".to_string(),
3222 end: ")".to_string(),
3223 close: true,
3224 surround: true,
3225 newline: true,
3226 },
3227 ],
3228 ..BracketPairConfig::default()
3229 },
3230 ..LanguageConfig::default()
3231 },
3232 Some(tree_sitter_rust::LANGUAGE.into()),
3233 )
3234 .with_brackets_query(
3235 r#"
3236 ("(" @open ")" @close)
3237 ("\"" @open "\"" @close)
3238 "#,
3239 )
3240 .unwrap(),
3241 );
3242
3243 let mut cx = EditorTestContext::new(cx).await;
3244 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3245
3246 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3247 cx.update_editor(|editor, window, cx| {
3248 editor.delete_to_previous_word_start(
3249 &DeleteToPreviousWordStart {
3250 ignore_newlines: true,
3251 ignore_brackets: false,
3252 },
3253 window,
3254 cx,
3255 );
3256 });
3257 // Deletion stops before brackets if asked to not ignore them.
3258 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
3259 cx.update_editor(|editor, window, cx| {
3260 editor.delete_to_previous_word_start(
3261 &DeleteToPreviousWordStart {
3262 ignore_newlines: true,
3263 ignore_brackets: false,
3264 },
3265 window,
3266 cx,
3267 );
3268 });
3269 // Deletion has to remove a single bracket and then stop again.
3270 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
3271
3272 cx.update_editor(|editor, window, cx| {
3273 editor.delete_to_previous_word_start(
3274 &DeleteToPreviousWordStart {
3275 ignore_newlines: true,
3276 ignore_brackets: false,
3277 },
3278 window,
3279 cx,
3280 );
3281 });
3282 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
3283
3284 cx.update_editor(|editor, window, cx| {
3285 editor.delete_to_previous_word_start(
3286 &DeleteToPreviousWordStart {
3287 ignore_newlines: true,
3288 ignore_brackets: false,
3289 },
3290 window,
3291 cx,
3292 );
3293 });
3294 cx.assert_editor_state(r#"ˇCOMMENT");"#);
3295
3296 cx.update_editor(|editor, window, cx| {
3297 editor.delete_to_previous_word_start(
3298 &DeleteToPreviousWordStart {
3299 ignore_newlines: true,
3300 ignore_brackets: false,
3301 },
3302 window,
3303 cx,
3304 );
3305 });
3306 cx.assert_editor_state(r#"ˇCOMMENT");"#);
3307
3308 cx.update_editor(|editor, window, cx| {
3309 editor.delete_to_next_word_end(
3310 &DeleteToNextWordEnd {
3311 ignore_newlines: true,
3312 ignore_brackets: false,
3313 },
3314 window,
3315 cx,
3316 );
3317 });
3318 // Brackets on the right are not paired anymore, hence deletion does not stop at them
3319 cx.assert_editor_state(r#"ˇ");"#);
3320
3321 cx.update_editor(|editor, window, cx| {
3322 editor.delete_to_next_word_end(
3323 &DeleteToNextWordEnd {
3324 ignore_newlines: true,
3325 ignore_brackets: false,
3326 },
3327 window,
3328 cx,
3329 );
3330 });
3331 cx.assert_editor_state(r#"ˇ"#);
3332
3333 cx.update_editor(|editor, window, cx| {
3334 editor.delete_to_next_word_end(
3335 &DeleteToNextWordEnd {
3336 ignore_newlines: true,
3337 ignore_brackets: false,
3338 },
3339 window,
3340 cx,
3341 );
3342 });
3343 cx.assert_editor_state(r#"ˇ"#);
3344
3345 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3346 cx.update_editor(|editor, window, cx| {
3347 editor.delete_to_previous_word_start(
3348 &DeleteToPreviousWordStart {
3349 ignore_newlines: true,
3350 ignore_brackets: true,
3351 },
3352 window,
3353 cx,
3354 );
3355 });
3356 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3357}
3358
3359#[gpui::test]
3360fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3361 init_test(cx, |_| {});
3362
3363 let editor = cx.add_window(|window, cx| {
3364 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3365 build_editor(buffer, window, cx)
3366 });
3367 let del_to_prev_word_start = DeleteToPreviousWordStart {
3368 ignore_newlines: false,
3369 ignore_brackets: false,
3370 };
3371 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3372 ignore_newlines: true,
3373 ignore_brackets: false,
3374 };
3375
3376 _ = editor.update(cx, |editor, window, cx| {
3377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3378 s.select_display_ranges([
3379 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3380 ])
3381 });
3382 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3383 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3384 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3385 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3386 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3387 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3388 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3389 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3390 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3391 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3392 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3393 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3394 });
3395}
3396
3397#[gpui::test]
3398fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
3399 init_test(cx, |_| {});
3400
3401 let editor = cx.add_window(|window, cx| {
3402 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
3403 build_editor(buffer, window, cx)
3404 });
3405 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
3406 ignore_newlines: false,
3407 ignore_brackets: false,
3408 };
3409 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
3410 ignore_newlines: true,
3411 ignore_brackets: false,
3412 };
3413
3414 _ = editor.update(cx, |editor, window, cx| {
3415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3416 s.select_display_ranges([
3417 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3418 ])
3419 });
3420 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3422 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3424 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3425 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3426 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3427 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3428 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3429 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3430 editor.delete_to_previous_subword_start(
3431 &del_to_prev_sub_word_start_ignore_newlines,
3432 window,
3433 cx,
3434 );
3435 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3436 });
3437}
3438
3439#[gpui::test]
3440fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3441 init_test(cx, |_| {});
3442
3443 let editor = cx.add_window(|window, cx| {
3444 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3445 build_editor(buffer, window, cx)
3446 });
3447 let del_to_next_word_end = DeleteToNextWordEnd {
3448 ignore_newlines: false,
3449 ignore_brackets: false,
3450 };
3451 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3452 ignore_newlines: true,
3453 ignore_brackets: false,
3454 };
3455
3456 _ = editor.update(cx, |editor, window, cx| {
3457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3458 s.select_display_ranges([
3459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3460 ])
3461 });
3462 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3463 assert_eq!(
3464 editor.buffer.read(cx).read(cx).text(),
3465 "one\n two\nthree\n four"
3466 );
3467 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3468 assert_eq!(
3469 editor.buffer.read(cx).read(cx).text(),
3470 "\n two\nthree\n four"
3471 );
3472 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3473 assert_eq!(
3474 editor.buffer.read(cx).read(cx).text(),
3475 "two\nthree\n four"
3476 );
3477 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3478 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3479 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3480 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3481 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3482 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3483 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3484 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3485 });
3486}
3487
3488#[gpui::test]
3489fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3490 init_test(cx, |_| {});
3491
3492 let editor = cx.add_window(|window, cx| {
3493 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3494 build_editor(buffer, window, cx)
3495 });
3496 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3497 ignore_newlines: false,
3498 ignore_brackets: false,
3499 };
3500 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3501 ignore_newlines: true,
3502 ignore_brackets: false,
3503 };
3504
3505 _ = editor.update(cx, |editor, window, cx| {
3506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3507 s.select_display_ranges([
3508 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3509 ])
3510 });
3511 // Delete "\n" (empty line)
3512 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3513 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3514 // Delete "foo" (subword boundary)
3515 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3516 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3517 // Delete "Bar"
3518 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3519 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3520 // Delete "\n " (newline + leading whitespace)
3521 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3522 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3523 // Delete "baz" (subword boundary)
3524 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3525 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3526 // With ignore_newlines, delete "Qux"
3527 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3528 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3529 });
3530}
3531
3532#[gpui::test]
3533fn test_newline(cx: &mut TestAppContext) {
3534 init_test(cx, |_| {});
3535
3536 let editor = cx.add_window(|window, cx| {
3537 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3538 build_editor(buffer, window, cx)
3539 });
3540
3541 _ = editor.update(cx, |editor, window, cx| {
3542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3543 s.select_display_ranges([
3544 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3545 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3546 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3547 ])
3548 });
3549
3550 editor.newline(&Newline, window, cx);
3551 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3552 });
3553}
3554
3555#[gpui::test]
3556async fn test_newline_yaml(cx: &mut TestAppContext) {
3557 init_test(cx, |_| {});
3558
3559 let mut cx = EditorTestContext::new(cx).await;
3560 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3562
3563 // Object (between 2 fields)
3564 cx.set_state(indoc! {"
3565 test:ˇ
3566 hello: bye"});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 test:
3570 ˇ
3571 hello: bye"});
3572
3573 // Object (first and single line)
3574 cx.set_state(indoc! {"
3575 test:ˇ"});
3576 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3577 cx.assert_editor_state(indoc! {"
3578 test:
3579 ˇ"});
3580
3581 // Array with objects (after first element)
3582 cx.set_state(indoc! {"
3583 test:
3584 - foo: barˇ"});
3585 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3586 cx.assert_editor_state(indoc! {"
3587 test:
3588 - foo: bar
3589 ˇ"});
3590
3591 // Array with objects and comment
3592 cx.set_state(indoc! {"
3593 test:
3594 - foo: bar
3595 - bar: # testˇ"});
3596 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3597 cx.assert_editor_state(indoc! {"
3598 test:
3599 - foo: bar
3600 - bar: # test
3601 ˇ"});
3602
3603 // Array with objects (after second element)
3604 cx.set_state(indoc! {"
3605 test:
3606 - foo: bar
3607 - bar: fooˇ"});
3608 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3609 cx.assert_editor_state(indoc! {"
3610 test:
3611 - foo: bar
3612 - bar: foo
3613 ˇ"});
3614
3615 // Array with strings (after first element)
3616 cx.set_state(indoc! {"
3617 test:
3618 - fooˇ"});
3619 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 test:
3622 - foo
3623 ˇ"});
3624}
3625
3626#[gpui::test]
3627fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3628 init_test(cx, |_| {});
3629
3630 let editor = cx.add_window(|window, cx| {
3631 let buffer = MultiBuffer::build_simple(
3632 "
3633 a
3634 b(
3635 X
3636 )
3637 c(
3638 X
3639 )
3640 "
3641 .unindent()
3642 .as_str(),
3643 cx,
3644 );
3645 let mut editor = build_editor(buffer, window, cx);
3646 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3647 s.select_ranges([
3648 Point::new(2, 4)..Point::new(2, 5),
3649 Point::new(5, 4)..Point::new(5, 5),
3650 ])
3651 });
3652 editor
3653 });
3654
3655 _ = editor.update(cx, |editor, window, cx| {
3656 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3657 editor.buffer.update(cx, |buffer, cx| {
3658 buffer.edit(
3659 [
3660 (Point::new(1, 2)..Point::new(3, 0), ""),
3661 (Point::new(4, 2)..Point::new(6, 0), ""),
3662 ],
3663 None,
3664 cx,
3665 );
3666 assert_eq!(
3667 buffer.read(cx).text(),
3668 "
3669 a
3670 b()
3671 c()
3672 "
3673 .unindent()
3674 );
3675 });
3676 assert_eq!(
3677 editor.selections.ranges(&editor.display_snapshot(cx)),
3678 &[
3679 Point::new(1, 2)..Point::new(1, 2),
3680 Point::new(2, 2)..Point::new(2, 2),
3681 ],
3682 );
3683
3684 editor.newline(&Newline, window, cx);
3685 assert_eq!(
3686 editor.text(cx),
3687 "
3688 a
3689 b(
3690 )
3691 c(
3692 )
3693 "
3694 .unindent()
3695 );
3696
3697 // The selections are moved after the inserted newlines
3698 assert_eq!(
3699 editor.selections.ranges(&editor.display_snapshot(cx)),
3700 &[
3701 Point::new(2, 0)..Point::new(2, 0),
3702 Point::new(4, 0)..Point::new(4, 0),
3703 ],
3704 );
3705 });
3706}
3707
3708#[gpui::test]
3709async fn test_newline_above(cx: &mut TestAppContext) {
3710 init_test(cx, |settings| {
3711 settings.defaults.tab_size = NonZeroU32::new(4)
3712 });
3713
3714 let language = Arc::new(
3715 Language::new(
3716 LanguageConfig::default(),
3717 Some(tree_sitter_rust::LANGUAGE.into()),
3718 )
3719 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3720 .unwrap(),
3721 );
3722
3723 let mut cx = EditorTestContext::new(cx).await;
3724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3725 cx.set_state(indoc! {"
3726 const a: ˇA = (
3727 (ˇ
3728 «const_functionˇ»(ˇ),
3729 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3730 )ˇ
3731 ˇ);ˇ
3732 "});
3733
3734 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3735 cx.assert_editor_state(indoc! {"
3736 ˇ
3737 const a: A = (
3738 ˇ
3739 (
3740 ˇ
3741 ˇ
3742 const_function(),
3743 ˇ
3744 ˇ
3745 ˇ
3746 ˇ
3747 something_else,
3748 ˇ
3749 )
3750 ˇ
3751 ˇ
3752 );
3753 "});
3754}
3755
3756#[gpui::test]
3757async fn test_newline_below(cx: &mut TestAppContext) {
3758 init_test(cx, |settings| {
3759 settings.defaults.tab_size = NonZeroU32::new(4)
3760 });
3761
3762 let language = Arc::new(
3763 Language::new(
3764 LanguageConfig::default(),
3765 Some(tree_sitter_rust::LANGUAGE.into()),
3766 )
3767 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3768 .unwrap(),
3769 );
3770
3771 let mut cx = EditorTestContext::new(cx).await;
3772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3773 cx.set_state(indoc! {"
3774 const a: ˇA = (
3775 (ˇ
3776 «const_functionˇ»(ˇ),
3777 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3778 )ˇ
3779 ˇ);ˇ
3780 "});
3781
3782 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3783 cx.assert_editor_state(indoc! {"
3784 const a: A = (
3785 ˇ
3786 (
3787 ˇ
3788 const_function(),
3789 ˇ
3790 ˇ
3791 something_else,
3792 ˇ
3793 ˇ
3794 ˇ
3795 ˇ
3796 )
3797 ˇ
3798 );
3799 ˇ
3800 ˇ
3801 "});
3802}
3803
3804#[gpui::test]
3805fn test_newline_respects_read_only(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let editor = cx.add_window(|window, cx| {
3809 let buffer = MultiBuffer::build_simple("aaaa\nbbbb\n", cx);
3810 build_editor(buffer, window, cx)
3811 });
3812
3813 _ = editor.update(cx, |editor, window, cx| {
3814 editor.set_read_only(true);
3815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3816 s.select_display_ranges([
3817 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2)
3818 ])
3819 });
3820
3821 editor.newline(&Newline, window, cx);
3822 assert_eq!(
3823 editor.text(cx),
3824 "aaaa\nbbbb\n",
3825 "newline should not modify a read-only editor"
3826 );
3827
3828 editor.newline_above(&NewlineAbove, window, cx);
3829 assert_eq!(
3830 editor.text(cx),
3831 "aaaa\nbbbb\n",
3832 "newline_above should not modify a read-only editor"
3833 );
3834
3835 editor.newline_below(&NewlineBelow, window, cx);
3836 assert_eq!(
3837 editor.text(cx),
3838 "aaaa\nbbbb\n",
3839 "newline_below should not modify a read-only editor"
3840 );
3841 });
3842}
3843
3844#[gpui::test]
3845fn test_newline_below_multibuffer(cx: &mut TestAppContext) {
3846 init_test(cx, |_| {});
3847
3848 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3849 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3850 let multibuffer = cx.new(|cx| {
3851 let mut multibuffer = MultiBuffer::new(ReadWrite);
3852 multibuffer.set_excerpts_for_path(
3853 PathKey::sorted(0),
3854 buffer_1.clone(),
3855 [Point::new(0, 0)..Point::new(2, 3)],
3856 0,
3857 cx,
3858 );
3859 multibuffer.set_excerpts_for_path(
3860 PathKey::sorted(1),
3861 buffer_2.clone(),
3862 [Point::new(0, 0)..Point::new(2, 3)],
3863 0,
3864 cx,
3865 );
3866 multibuffer
3867 });
3868
3869 cx.add_window(|window, cx| {
3870 let mut editor = build_editor(multibuffer, window, cx);
3871
3872 assert_eq!(
3873 editor.text(cx),
3874 indoc! {"
3875 aaa
3876 bbb
3877 ccc
3878 ddd
3879 eee
3880 fff"}
3881 );
3882
3883 // Cursor on the last line of the first excerpt.
3884 // The newline should be inserted within the first excerpt (buffer_1),
3885 // not in the second excerpt (buffer_2).
3886 select_ranges(
3887 &mut editor,
3888 indoc! {"
3889 aaa
3890 bbb
3891 cˇcc
3892 ddd
3893 eee
3894 fff"},
3895 window,
3896 cx,
3897 );
3898 editor.newline_below(&NewlineBelow, window, cx);
3899 assert_text_with_selections(
3900 &mut editor,
3901 indoc! {"
3902 aaa
3903 bbb
3904 ccc
3905 ˇ
3906 ddd
3907 eee
3908 fff"},
3909 cx,
3910 );
3911 buffer_1.read_with(cx, |buffer, _| {
3912 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
3913 });
3914 buffer_2.read_with(cx, |buffer, _| {
3915 assert_eq!(buffer.text(), "ddd\neee\nfff");
3916 });
3917
3918 editor
3919 });
3920}
3921
3922#[gpui::test]
3923fn test_newline_below_multibuffer_middle_of_excerpt(cx: &mut TestAppContext) {
3924 init_test(cx, |_| {});
3925
3926 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3927 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3928 let multibuffer = cx.new(|cx| {
3929 let mut multibuffer = MultiBuffer::new(ReadWrite);
3930 multibuffer.set_excerpts_for_path(
3931 PathKey::sorted(0),
3932 buffer_1.clone(),
3933 [Point::new(0, 0)..Point::new(2, 3)],
3934 0,
3935 cx,
3936 );
3937 multibuffer.set_excerpts_for_path(
3938 PathKey::sorted(1),
3939 buffer_2.clone(),
3940 [Point::new(0, 0)..Point::new(2, 3)],
3941 0,
3942 cx,
3943 );
3944 multibuffer
3945 });
3946
3947 cx.add_window(|window, cx| {
3948 let mut editor = build_editor(multibuffer, window, cx);
3949
3950 // Cursor in the middle of the first excerpt.
3951 select_ranges(
3952 &mut editor,
3953 indoc! {"
3954 aˇaa
3955 bbb
3956 ccc
3957 ddd
3958 eee
3959 fff"},
3960 window,
3961 cx,
3962 );
3963 editor.newline_below(&NewlineBelow, window, cx);
3964 assert_text_with_selections(
3965 &mut editor,
3966 indoc! {"
3967 aaa
3968 ˇ
3969 bbb
3970 ccc
3971 ddd
3972 eee
3973 fff"},
3974 cx,
3975 );
3976 buffer_1.read_with(cx, |buffer, _| {
3977 assert_eq!(buffer.text(), "aaa\n\nbbb\nccc");
3978 });
3979 buffer_2.read_with(cx, |buffer, _| {
3980 assert_eq!(buffer.text(), "ddd\neee\nfff");
3981 });
3982
3983 editor
3984 });
3985}
3986
3987#[gpui::test]
3988fn test_newline_below_multibuffer_last_line_of_last_excerpt(cx: &mut TestAppContext) {
3989 init_test(cx, |_| {});
3990
3991 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3992 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3993 let multibuffer = cx.new(|cx| {
3994 let mut multibuffer = MultiBuffer::new(ReadWrite);
3995 multibuffer.set_excerpts_for_path(
3996 PathKey::sorted(0),
3997 buffer_1.clone(),
3998 [Point::new(0, 0)..Point::new(2, 3)],
3999 0,
4000 cx,
4001 );
4002 multibuffer.set_excerpts_for_path(
4003 PathKey::sorted(1),
4004 buffer_2.clone(),
4005 [Point::new(0, 0)..Point::new(2, 3)],
4006 0,
4007 cx,
4008 );
4009 multibuffer
4010 });
4011
4012 cx.add_window(|window, cx| {
4013 let mut editor = build_editor(multibuffer, window, cx);
4014
4015 // Cursor on the last line of the last excerpt.
4016 select_ranges(
4017 &mut editor,
4018 indoc! {"
4019 aaa
4020 bbb
4021 ccc
4022 ddd
4023 eee
4024 fˇff"},
4025 window,
4026 cx,
4027 );
4028 editor.newline_below(&NewlineBelow, window, cx);
4029 assert_text_with_selections(
4030 &mut editor,
4031 indoc! {"
4032 aaa
4033 bbb
4034 ccc
4035 ddd
4036 eee
4037 fff
4038 ˇ"},
4039 cx,
4040 );
4041 buffer_1.read_with(cx, |buffer, _| {
4042 assert_eq!(buffer.text(), "aaa\nbbb\nccc");
4043 });
4044 buffer_2.read_with(cx, |buffer, _| {
4045 assert_eq!(buffer.text(), "ddd\neee\nfff\n");
4046 });
4047
4048 editor
4049 });
4050}
4051
4052#[gpui::test]
4053fn test_newline_below_multibuffer_multiple_cursors(cx: &mut TestAppContext) {
4054 init_test(cx, |_| {});
4055
4056 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
4057 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
4058 let multibuffer = cx.new(|cx| {
4059 let mut multibuffer = MultiBuffer::new(ReadWrite);
4060 multibuffer.set_excerpts_for_path(
4061 PathKey::sorted(0),
4062 buffer_1.clone(),
4063 [Point::new(0, 0)..Point::new(2, 3)],
4064 0,
4065 cx,
4066 );
4067 multibuffer.set_excerpts_for_path(
4068 PathKey::sorted(1),
4069 buffer_2.clone(),
4070 [Point::new(0, 0)..Point::new(2, 3)],
4071 0,
4072 cx,
4073 );
4074 multibuffer
4075 });
4076
4077 cx.add_window(|window, cx| {
4078 let mut editor = build_editor(multibuffer, window, cx);
4079
4080 // Cursors on the last line of the first excerpt and the first line
4081 // of the second excerpt. Each newline should go into its respective buffer.
4082 select_ranges(
4083 &mut editor,
4084 indoc! {"
4085 aaa
4086 bbb
4087 cˇcc
4088 dˇdd
4089 eee
4090 fff"},
4091 window,
4092 cx,
4093 );
4094 editor.newline_below(&NewlineBelow, window, cx);
4095 assert_text_with_selections(
4096 &mut editor,
4097 indoc! {"
4098 aaa
4099 bbb
4100 ccc
4101 ˇ
4102 ddd
4103 ˇ
4104 eee
4105 fff"},
4106 cx,
4107 );
4108 buffer_1.read_with(cx, |buffer, _| {
4109 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
4110 });
4111 buffer_2.read_with(cx, |buffer, _| {
4112 assert_eq!(buffer.text(), "ddd\n\neee\nfff");
4113 });
4114
4115 editor
4116 });
4117}
4118
4119#[gpui::test]
4120async fn test_newline_comments(cx: &mut TestAppContext) {
4121 init_test(cx, |settings| {
4122 settings.defaults.tab_size = NonZeroU32::new(4)
4123 });
4124
4125 let language = Arc::new(Language::new(
4126 LanguageConfig {
4127 line_comments: vec!["// ".into()],
4128 ..LanguageConfig::default()
4129 },
4130 None,
4131 ));
4132 {
4133 let mut cx = EditorTestContext::new(cx).await;
4134 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4135 cx.set_state(indoc! {"
4136 // Fooˇ
4137 "});
4138
4139 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4140 cx.assert_editor_state(indoc! {"
4141 // Foo
4142 // ˇ
4143 "});
4144 // Ensure that we add comment prefix when existing line contains space
4145 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4146 cx.assert_editor_state(
4147 indoc! {"
4148 // Foo
4149 //s
4150 // ˇ
4151 "}
4152 .replace("s", " ") // s is used as space placeholder to prevent format on save
4153 .as_str(),
4154 );
4155 // Ensure that we add comment prefix when existing line does not contain space
4156 cx.set_state(indoc! {"
4157 // Foo
4158 //ˇ
4159 "});
4160 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4161 cx.assert_editor_state(indoc! {"
4162 // Foo
4163 //
4164 // ˇ
4165 "});
4166 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
4167 cx.set_state(indoc! {"
4168 ˇ// Foo
4169 "});
4170 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4171 cx.assert_editor_state(indoc! {"
4172
4173 ˇ// Foo
4174 "});
4175 }
4176 // Ensure that comment continuations can be disabled.
4177 update_test_language_settings(cx, &|settings| {
4178 settings.defaults.extend_comment_on_newline = Some(false);
4179 });
4180 let mut cx = EditorTestContext::new(cx).await;
4181 cx.set_state(indoc! {"
4182 // Fooˇ
4183 "});
4184 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 // Foo
4187 ˇ
4188 "});
4189}
4190
4191#[gpui::test]
4192async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
4193 init_test(cx, |settings| {
4194 settings.defaults.tab_size = NonZeroU32::new(4)
4195 });
4196
4197 let language = Arc::new(Language::new(
4198 LanguageConfig {
4199 line_comments: vec!["// ".into(), "/// ".into()],
4200 ..LanguageConfig::default()
4201 },
4202 None,
4203 ));
4204 {
4205 let mut cx = EditorTestContext::new(cx).await;
4206 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4207 cx.set_state(indoc! {"
4208 //ˇ
4209 "});
4210 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4211 cx.assert_editor_state(indoc! {"
4212 //
4213 // ˇ
4214 "});
4215
4216 cx.set_state(indoc! {"
4217 ///ˇ
4218 "});
4219 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4220 cx.assert_editor_state(indoc! {"
4221 ///
4222 /// ˇ
4223 "});
4224 }
4225}
4226
4227#[gpui::test]
4228async fn test_newline_comments_repl_separators(cx: &mut TestAppContext) {
4229 init_test(cx, |settings| {
4230 settings.defaults.tab_size = NonZeroU32::new(4)
4231 });
4232 let language = Arc::new(Language::new(
4233 LanguageConfig {
4234 line_comments: vec!["# ".into()],
4235 ..LanguageConfig::default()
4236 },
4237 None,
4238 ));
4239
4240 {
4241 let mut cx = EditorTestContext::new(cx).await;
4242 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4243 cx.set_state(indoc! {"
4244 # %%ˇ
4245 "});
4246 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4247 cx.assert_editor_state(indoc! {"
4248 # %%
4249 ˇ
4250 "});
4251
4252 cx.set_state(indoc! {"
4253 # %%%%%ˇ
4254 "});
4255 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4256 cx.assert_editor_state(indoc! {"
4257 # %%%%%
4258 ˇ
4259 "});
4260
4261 cx.set_state(indoc! {"
4262 # %ˇ%
4263 "});
4264 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4265 cx.assert_editor_state(indoc! {"
4266 # %
4267 # ˇ%
4268 "});
4269 }
4270}
4271
4272#[gpui::test]
4273async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
4274 init_test(cx, |settings| {
4275 settings.defaults.tab_size = NonZeroU32::new(4)
4276 });
4277
4278 let language = Arc::new(
4279 Language::new(
4280 LanguageConfig {
4281 documentation_comment: Some(language::BlockCommentConfig {
4282 start: "/**".into(),
4283 end: "*/".into(),
4284 prefix: "* ".into(),
4285 tab_size: 1,
4286 }),
4287
4288 ..LanguageConfig::default()
4289 },
4290 Some(tree_sitter_rust::LANGUAGE.into()),
4291 )
4292 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
4293 .unwrap(),
4294 );
4295
4296 {
4297 let mut cx = EditorTestContext::new(cx).await;
4298 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4299 cx.set_state(indoc! {"
4300 /**ˇ
4301 "});
4302
4303 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4304 cx.assert_editor_state(indoc! {"
4305 /**
4306 * ˇ
4307 "});
4308 // Ensure that if cursor is before the comment start,
4309 // we do not actually insert a comment prefix.
4310 cx.set_state(indoc! {"
4311 ˇ/**
4312 "});
4313 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4314 cx.assert_editor_state(indoc! {"
4315
4316 ˇ/**
4317 "});
4318 // Ensure that if cursor is between it doesn't add comment prefix.
4319 cx.set_state(indoc! {"
4320 /*ˇ*
4321 "});
4322 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 /*
4325 ˇ*
4326 "});
4327 // Ensure that if suffix exists on same line after cursor it adds new line.
4328 cx.set_state(indoc! {"
4329 /**ˇ*/
4330 "});
4331 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 /**
4334 * ˇ
4335 */
4336 "});
4337 // Ensure that if suffix exists on same line after cursor with space it adds new line.
4338 cx.set_state(indoc! {"
4339 /**ˇ */
4340 "});
4341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 /**
4344 * ˇ
4345 */
4346 "});
4347 // Ensure that if suffix exists on same line after cursor with space it adds new line.
4348 cx.set_state(indoc! {"
4349 /** ˇ*/
4350 "});
4351 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4352 cx.assert_editor_state(
4353 indoc! {"
4354 /**s
4355 * ˇ
4356 */
4357 "}
4358 .replace("s", " ") // s is used as space placeholder to prevent format on save
4359 .as_str(),
4360 );
4361 // Ensure that delimiter space is preserved when newline on already
4362 // spaced delimiter.
4363 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4364 cx.assert_editor_state(
4365 indoc! {"
4366 /**s
4367 *s
4368 * ˇ
4369 */
4370 "}
4371 .replace("s", " ") // s is used as space placeholder to prevent format on save
4372 .as_str(),
4373 );
4374 // Ensure that delimiter space is preserved when space is not
4375 // on existing delimiter.
4376 cx.set_state(indoc! {"
4377 /**
4378 *ˇ
4379 */
4380 "});
4381 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4382 cx.assert_editor_state(indoc! {"
4383 /**
4384 *
4385 * ˇ
4386 */
4387 "});
4388 // Ensure that if suffix exists on same line after cursor it
4389 // doesn't add extra new line if prefix is not on same line.
4390 cx.set_state(indoc! {"
4391 /**
4392 ˇ*/
4393 "});
4394 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4395 cx.assert_editor_state(indoc! {"
4396 /**
4397
4398 ˇ*/
4399 "});
4400 // Ensure that it detects suffix after existing prefix.
4401 cx.set_state(indoc! {"
4402 /**ˇ/
4403 "});
4404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4405 cx.assert_editor_state(indoc! {"
4406 /**
4407 ˇ/
4408 "});
4409 // Ensure that if suffix exists on same line before
4410 // cursor it does not add comment prefix.
4411 cx.set_state(indoc! {"
4412 /** */ˇ
4413 "});
4414 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4415 cx.assert_editor_state(indoc! {"
4416 /** */
4417 ˇ
4418 "});
4419 // Ensure that if suffix exists on same line before
4420 // cursor it does not add comment prefix.
4421 cx.set_state(indoc! {"
4422 /**
4423 *
4424 */ˇ
4425 "});
4426 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4427 cx.assert_editor_state(indoc! {"
4428 /**
4429 *
4430 */
4431 ˇ
4432 "});
4433
4434 // Ensure that inline comment followed by code
4435 // doesn't add comment prefix on newline
4436 cx.set_state(indoc! {"
4437 /** */ textˇ
4438 "});
4439 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4440 cx.assert_editor_state(indoc! {"
4441 /** */ text
4442 ˇ
4443 "});
4444
4445 // Ensure that text after comment end tag
4446 // doesn't add comment prefix on newline
4447 cx.set_state(indoc! {"
4448 /**
4449 *
4450 */ˇtext
4451 "});
4452 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4453 cx.assert_editor_state(indoc! {"
4454 /**
4455 *
4456 */
4457 ˇtext
4458 "});
4459
4460 // Ensure if not comment block it doesn't
4461 // add comment prefix on newline
4462 cx.set_state(indoc! {"
4463 * textˇ
4464 "});
4465 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4466 cx.assert_editor_state(indoc! {"
4467 * text
4468 ˇ
4469 "});
4470 }
4471 // Ensure that comment continuations can be disabled.
4472 update_test_language_settings(cx, &|settings| {
4473 settings.defaults.extend_comment_on_newline = Some(false);
4474 });
4475 let mut cx = EditorTestContext::new(cx).await;
4476 cx.set_state(indoc! {"
4477 /**ˇ
4478 "});
4479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4480 cx.assert_editor_state(indoc! {"
4481 /**
4482 ˇ
4483 "});
4484}
4485
4486#[gpui::test]
4487async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
4488 init_test(cx, |settings| {
4489 settings.defaults.tab_size = NonZeroU32::new(4)
4490 });
4491
4492 let lua_language = Arc::new(Language::new(
4493 LanguageConfig {
4494 line_comments: vec!["--".into()],
4495 block_comment: Some(language::BlockCommentConfig {
4496 start: "--[[".into(),
4497 prefix: "".into(),
4498 end: "]]".into(),
4499 tab_size: 0,
4500 }),
4501 ..LanguageConfig::default()
4502 },
4503 None,
4504 ));
4505
4506 let mut cx = EditorTestContext::new(cx).await;
4507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
4508
4509 // Line with line comment should extend
4510 cx.set_state(indoc! {"
4511 --ˇ
4512 "});
4513 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4514 cx.assert_editor_state(indoc! {"
4515 --
4516 --ˇ
4517 "});
4518
4519 // Line with block comment that matches line comment should not extend
4520 cx.set_state(indoc! {"
4521 --[[ˇ
4522 "});
4523 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4524 cx.assert_editor_state(indoc! {"
4525 --[[
4526 ˇ
4527 "});
4528}
4529
4530#[gpui::test]
4531fn test_insert_with_old_selections(cx: &mut TestAppContext) {
4532 init_test(cx, |_| {});
4533
4534 let editor = cx.add_window(|window, cx| {
4535 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
4536 let mut editor = build_editor(buffer, window, cx);
4537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4538 s.select_ranges([
4539 MultiBufferOffset(3)..MultiBufferOffset(4),
4540 MultiBufferOffset(11)..MultiBufferOffset(12),
4541 MultiBufferOffset(19)..MultiBufferOffset(20),
4542 ])
4543 });
4544 editor
4545 });
4546
4547 _ = editor.update(cx, |editor, window, cx| {
4548 // Edit the buffer directly, deleting ranges surrounding the editor's selections
4549 editor.buffer.update(cx, |buffer, cx| {
4550 buffer.edit(
4551 [
4552 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
4553 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
4554 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
4555 ],
4556 None,
4557 cx,
4558 );
4559 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
4560 });
4561 assert_eq!(
4562 editor.selections.ranges(&editor.display_snapshot(cx)),
4563 &[
4564 MultiBufferOffset(2)..MultiBufferOffset(2),
4565 MultiBufferOffset(7)..MultiBufferOffset(7),
4566 MultiBufferOffset(12)..MultiBufferOffset(12)
4567 ],
4568 );
4569
4570 editor.insert("Z", window, cx);
4571 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
4572
4573 // The selections are moved after the inserted characters
4574 assert_eq!(
4575 editor.selections.ranges(&editor.display_snapshot(cx)),
4576 &[
4577 MultiBufferOffset(3)..MultiBufferOffset(3),
4578 MultiBufferOffset(9)..MultiBufferOffset(9),
4579 MultiBufferOffset(15)..MultiBufferOffset(15)
4580 ],
4581 );
4582 });
4583}
4584
4585#[gpui::test]
4586async fn test_tab(cx: &mut TestAppContext) {
4587 init_test(cx, |settings| {
4588 settings.defaults.tab_size = NonZeroU32::new(3)
4589 });
4590
4591 let mut cx = EditorTestContext::new(cx).await;
4592 cx.set_state(indoc! {"
4593 ˇabˇc
4594 ˇ🏀ˇ🏀ˇefg
4595 dˇ
4596 "});
4597 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4598 cx.assert_editor_state(indoc! {"
4599 ˇab ˇc
4600 ˇ🏀 ˇ🏀 ˇefg
4601 d ˇ
4602 "});
4603
4604 cx.set_state(indoc! {"
4605 a
4606 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4607 "});
4608 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4609 cx.assert_editor_state(indoc! {"
4610 a
4611 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4612 "});
4613}
4614
4615#[gpui::test]
4616async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
4617 init_test(cx, |_| {});
4618
4619 let mut cx = EditorTestContext::new(cx).await;
4620 let language = Arc::new(
4621 Language::new(
4622 LanguageConfig::default(),
4623 Some(tree_sitter_rust::LANGUAGE.into()),
4624 )
4625 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
4626 .unwrap(),
4627 );
4628 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4629
4630 // test when all cursors are not at suggested indent
4631 // then simply move to their suggested indent location
4632 cx.set_state(indoc! {"
4633 const a: B = (
4634 c(
4635 ˇ
4636 ˇ )
4637 );
4638 "});
4639 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4640 cx.assert_editor_state(indoc! {"
4641 const a: B = (
4642 c(
4643 ˇ
4644 ˇ)
4645 );
4646 "});
4647
4648 // test cursor already at suggested indent not moving when
4649 // other cursors are yet to reach their suggested indents
4650 cx.set_state(indoc! {"
4651 ˇ
4652 const a: B = (
4653 c(
4654 d(
4655 ˇ
4656 )
4657 ˇ
4658 ˇ )
4659 );
4660 "});
4661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4662 cx.assert_editor_state(indoc! {"
4663 ˇ
4664 const a: B = (
4665 c(
4666 d(
4667 ˇ
4668 )
4669 ˇ
4670 ˇ)
4671 );
4672 "});
4673 // test when all cursors are at suggested indent then tab is inserted
4674 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4675 cx.assert_editor_state(indoc! {"
4676 ˇ
4677 const a: B = (
4678 c(
4679 d(
4680 ˇ
4681 )
4682 ˇ
4683 ˇ)
4684 );
4685 "});
4686
4687 // test when current indent is less than suggested indent,
4688 // we adjust line to match suggested indent and move cursor to it
4689 //
4690 // when no other cursor is at word boundary, all of them should move
4691 cx.set_state(indoc! {"
4692 const a: B = (
4693 c(
4694 d(
4695 ˇ
4696 ˇ )
4697 ˇ )
4698 );
4699 "});
4700 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4701 cx.assert_editor_state(indoc! {"
4702 const a: B = (
4703 c(
4704 d(
4705 ˇ
4706 ˇ)
4707 ˇ)
4708 );
4709 "});
4710
4711 // test when current indent is less than suggested indent,
4712 // we adjust line to match suggested indent and move cursor to it
4713 //
4714 // when some other cursor is at word boundary, it should not move
4715 cx.set_state(indoc! {"
4716 const a: B = (
4717 c(
4718 d(
4719 ˇ
4720 ˇ )
4721 ˇ)
4722 );
4723 "});
4724 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4725 cx.assert_editor_state(indoc! {"
4726 const a: B = (
4727 c(
4728 d(
4729 ˇ
4730 ˇ)
4731 ˇ)
4732 );
4733 "});
4734
4735 // test when current indent is more than suggested indent,
4736 // we just move cursor to current indent instead of suggested indent
4737 //
4738 // when no other cursor is at word boundary, all of them should move
4739 cx.set_state(indoc! {"
4740 const a: B = (
4741 c(
4742 d(
4743 ˇ
4744 ˇ )
4745 ˇ )
4746 );
4747 "});
4748 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4749 cx.assert_editor_state(indoc! {"
4750 const a: B = (
4751 c(
4752 d(
4753 ˇ
4754 ˇ)
4755 ˇ)
4756 );
4757 "});
4758 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4759 cx.assert_editor_state(indoc! {"
4760 const a: B = (
4761 c(
4762 d(
4763 ˇ
4764 ˇ)
4765 ˇ)
4766 );
4767 "});
4768
4769 // test when current indent is more than suggested indent,
4770 // we just move cursor to current indent instead of suggested indent
4771 //
4772 // when some other cursor is at word boundary, it doesn't move
4773 cx.set_state(indoc! {"
4774 const a: B = (
4775 c(
4776 d(
4777 ˇ
4778 ˇ )
4779 ˇ)
4780 );
4781 "});
4782 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4783 cx.assert_editor_state(indoc! {"
4784 const a: B = (
4785 c(
4786 d(
4787 ˇ
4788 ˇ)
4789 ˇ)
4790 );
4791 "});
4792
4793 // handle auto-indent when there are multiple cursors on the same line
4794 cx.set_state(indoc! {"
4795 const a: B = (
4796 c(
4797 ˇ ˇ
4798 ˇ )
4799 );
4800 "});
4801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4802 cx.assert_editor_state(indoc! {"
4803 const a: B = (
4804 c(
4805 ˇ
4806 ˇ)
4807 );
4808 "});
4809}
4810
4811#[gpui::test]
4812async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4813 init_test(cx, |settings| {
4814 settings.defaults.tab_size = NonZeroU32::new(3)
4815 });
4816
4817 let mut cx = EditorTestContext::new(cx).await;
4818 cx.set_state(indoc! {"
4819 ˇ
4820 \t ˇ
4821 \t ˇ
4822 \t ˇ
4823 \t \t\t \t \t\t \t\t \t \t ˇ
4824 "});
4825
4826 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4827 cx.assert_editor_state(indoc! {"
4828 ˇ
4829 \t ˇ
4830 \t ˇ
4831 \t ˇ
4832 \t \t\t \t \t\t \t\t \t \t ˇ
4833 "});
4834}
4835
4836#[gpui::test]
4837async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4838 init_test(cx, |settings| {
4839 settings.defaults.tab_size = NonZeroU32::new(4)
4840 });
4841
4842 let language = Arc::new(
4843 Language::new(
4844 LanguageConfig::default(),
4845 Some(tree_sitter_rust::LANGUAGE.into()),
4846 )
4847 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4848 .unwrap(),
4849 );
4850
4851 let mut cx = EditorTestContext::new(cx).await;
4852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4853 cx.set_state(indoc! {"
4854 fn a() {
4855 if b {
4856 \t ˇc
4857 }
4858 }
4859 "});
4860
4861 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4862 cx.assert_editor_state(indoc! {"
4863 fn a() {
4864 if b {
4865 ˇc
4866 }
4867 }
4868 "});
4869}
4870
4871#[gpui::test]
4872async fn test_indent_outdent(cx: &mut TestAppContext) {
4873 init_test(cx, |settings| {
4874 settings.defaults.tab_size = NonZeroU32::new(4);
4875 });
4876
4877 let mut cx = EditorTestContext::new(cx).await;
4878
4879 cx.set_state(indoc! {"
4880 «oneˇ» «twoˇ»
4881 three
4882 four
4883 "});
4884 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4885 cx.assert_editor_state(indoc! {"
4886 «oneˇ» «twoˇ»
4887 three
4888 four
4889 "});
4890
4891 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4892 cx.assert_editor_state(indoc! {"
4893 «oneˇ» «twoˇ»
4894 three
4895 four
4896 "});
4897
4898 // select across line ending
4899 cx.set_state(indoc! {"
4900 one two
4901 t«hree
4902 ˇ» four
4903 "});
4904 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4905 cx.assert_editor_state(indoc! {"
4906 one two
4907 t«hree
4908 ˇ» four
4909 "});
4910
4911 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4912 cx.assert_editor_state(indoc! {"
4913 one two
4914 t«hree
4915 ˇ» four
4916 "});
4917
4918 // Ensure that indenting/outdenting works when the cursor is at column 0.
4919 cx.set_state(indoc! {"
4920 one two
4921 ˇthree
4922 four
4923 "});
4924 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4925 cx.assert_editor_state(indoc! {"
4926 one two
4927 ˇthree
4928 four
4929 "});
4930
4931 cx.set_state(indoc! {"
4932 one two
4933 ˇ three
4934 four
4935 "});
4936 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4937 cx.assert_editor_state(indoc! {"
4938 one two
4939 ˇthree
4940 four
4941 "});
4942}
4943
4944#[gpui::test]
4945async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4946 // This is a regression test for issue #33761
4947 init_test(cx, |_| {});
4948
4949 let mut cx = EditorTestContext::new(cx).await;
4950 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4951 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4952
4953 cx.set_state(
4954 r#"ˇ# ingress:
4955ˇ# api:
4956ˇ# enabled: false
4957ˇ# pathType: Prefix
4958ˇ# console:
4959ˇ# enabled: false
4960ˇ# pathType: Prefix
4961"#,
4962 );
4963
4964 // Press tab to indent all lines
4965 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4966
4967 cx.assert_editor_state(
4968 r#" ˇ# ingress:
4969 ˇ# api:
4970 ˇ# enabled: false
4971 ˇ# pathType: Prefix
4972 ˇ# console:
4973 ˇ# enabled: false
4974 ˇ# pathType: Prefix
4975"#,
4976 );
4977}
4978
4979#[gpui::test]
4980async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4981 // This is a test to make sure our fix for issue #33761 didn't break anything
4982 init_test(cx, |_| {});
4983
4984 let mut cx = EditorTestContext::new(cx).await;
4985 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4987
4988 cx.set_state(
4989 r#"ˇingress:
4990ˇ api:
4991ˇ enabled: false
4992ˇ pathType: Prefix
4993"#,
4994 );
4995
4996 // Press tab to indent all lines
4997 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4998
4999 cx.assert_editor_state(
5000 r#"ˇingress:
5001 ˇapi:
5002 ˇenabled: false
5003 ˇpathType: Prefix
5004"#,
5005 );
5006}
5007
5008#[gpui::test]
5009async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
5010 init_test(cx, |settings| {
5011 settings.defaults.hard_tabs = Some(true);
5012 });
5013
5014 let mut cx = EditorTestContext::new(cx).await;
5015
5016 // select two ranges on one line
5017 cx.set_state(indoc! {"
5018 «oneˇ» «twoˇ»
5019 three
5020 four
5021 "});
5022 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
5023 cx.assert_editor_state(indoc! {"
5024 \t«oneˇ» «twoˇ»
5025 three
5026 four
5027 "});
5028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
5029 cx.assert_editor_state(indoc! {"
5030 \t\t«oneˇ» «twoˇ»
5031 three
5032 four
5033 "});
5034 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5035 cx.assert_editor_state(indoc! {"
5036 \t«oneˇ» «twoˇ»
5037 three
5038 four
5039 "});
5040 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5041 cx.assert_editor_state(indoc! {"
5042 «oneˇ» «twoˇ»
5043 three
5044 four
5045 "});
5046
5047 // select across a line ending
5048 cx.set_state(indoc! {"
5049 one two
5050 t«hree
5051 ˇ»four
5052 "});
5053 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
5054 cx.assert_editor_state(indoc! {"
5055 one two
5056 \tt«hree
5057 ˇ»four
5058 "});
5059 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
5060 cx.assert_editor_state(indoc! {"
5061 one two
5062 \t\tt«hree
5063 ˇ»four
5064 "});
5065 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5066 cx.assert_editor_state(indoc! {"
5067 one two
5068 \tt«hree
5069 ˇ»four
5070 "});
5071 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5072 cx.assert_editor_state(indoc! {"
5073 one two
5074 t«hree
5075 ˇ»four
5076 "});
5077
5078 // Ensure that indenting/outdenting works when the cursor is at column 0.
5079 cx.set_state(indoc! {"
5080 one two
5081 ˇthree
5082 four
5083 "});
5084 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5085 cx.assert_editor_state(indoc! {"
5086 one two
5087 ˇthree
5088 four
5089 "});
5090 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
5091 cx.assert_editor_state(indoc! {"
5092 one two
5093 \tˇthree
5094 four
5095 "});
5096 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
5097 cx.assert_editor_state(indoc! {"
5098 one two
5099 ˇthree
5100 four
5101 "});
5102}
5103
5104#[gpui::test]
5105fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
5106 init_test(cx, |settings| {
5107 settings.languages.0.extend([
5108 (
5109 "TOML".into(),
5110 LanguageSettingsContent {
5111 tab_size: NonZeroU32::new(2),
5112 ..Default::default()
5113 },
5114 ),
5115 (
5116 "Rust".into(),
5117 LanguageSettingsContent {
5118 tab_size: NonZeroU32::new(4),
5119 ..Default::default()
5120 },
5121 ),
5122 ]);
5123 });
5124
5125 let toml_language = Arc::new(Language::new(
5126 LanguageConfig {
5127 name: "TOML".into(),
5128 ..Default::default()
5129 },
5130 None,
5131 ));
5132 let rust_language = Arc::new(Language::new(
5133 LanguageConfig {
5134 name: "Rust".into(),
5135 ..Default::default()
5136 },
5137 None,
5138 ));
5139
5140 let toml_buffer =
5141 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
5142 let rust_buffer =
5143 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
5144 let multibuffer = cx.new(|cx| {
5145 let mut multibuffer = MultiBuffer::new(ReadWrite);
5146 multibuffer.set_excerpts_for_path(
5147 PathKey::sorted(0),
5148 toml_buffer.clone(),
5149 [Point::new(0, 0)..Point::new(2, 0)],
5150 0,
5151 cx,
5152 );
5153 multibuffer.set_excerpts_for_path(
5154 PathKey::sorted(1),
5155 rust_buffer.clone(),
5156 [Point::new(0, 0)..Point::new(1, 0)],
5157 0,
5158 cx,
5159 );
5160 multibuffer
5161 });
5162
5163 cx.add_window(|window, cx| {
5164 let mut editor = build_editor(multibuffer, window, cx);
5165
5166 assert_eq!(
5167 editor.text(cx),
5168 indoc! {"
5169 a = 1
5170 b = 2
5171
5172 const c: usize = 3;
5173 "}
5174 );
5175
5176 select_ranges(
5177 &mut editor,
5178 indoc! {"
5179 «aˇ» = 1
5180 b = 2
5181
5182 «const c:ˇ» usize = 3;
5183 "},
5184 window,
5185 cx,
5186 );
5187
5188 editor.tab(&Tab, window, cx);
5189 assert_text_with_selections(
5190 &mut editor,
5191 indoc! {"
5192 «aˇ» = 1
5193 b = 2
5194
5195 «const c:ˇ» usize = 3;
5196 "},
5197 cx,
5198 );
5199 editor.backtab(&Backtab, window, cx);
5200 assert_text_with_selections(
5201 &mut editor,
5202 indoc! {"
5203 «aˇ» = 1
5204 b = 2
5205
5206 «const c:ˇ» usize = 3;
5207 "},
5208 cx,
5209 );
5210
5211 editor
5212 });
5213}
5214
5215#[gpui::test]
5216async fn test_backspace(cx: &mut TestAppContext) {
5217 init_test(cx, |_| {});
5218
5219 let mut cx = EditorTestContext::new(cx).await;
5220
5221 // Basic backspace
5222 cx.set_state(indoc! {"
5223 onˇe two three
5224 fou«rˇ» five six
5225 seven «ˇeight nine
5226 »ten
5227 "});
5228 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
5229 cx.assert_editor_state(indoc! {"
5230 oˇe two three
5231 fouˇ five six
5232 seven ˇten
5233 "});
5234
5235 // Test backspace inside and around indents
5236 cx.set_state(indoc! {"
5237 zero
5238 ˇone
5239 ˇtwo
5240 ˇ ˇ ˇ three
5241 ˇ ˇ four
5242 "});
5243 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
5244 cx.assert_editor_state(indoc! {"
5245 zero
5246 ˇone
5247 ˇtwo
5248 ˇ threeˇ four
5249 "});
5250}
5251
5252#[gpui::test]
5253async fn test_delete(cx: &mut TestAppContext) {
5254 init_test(cx, |_| {});
5255
5256 let mut cx = EditorTestContext::new(cx).await;
5257 cx.set_state(indoc! {"
5258 onˇe two three
5259 fou«rˇ» five six
5260 seven «ˇeight nine
5261 »ten
5262 "});
5263 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
5264 cx.assert_editor_state(indoc! {"
5265 onˇ two three
5266 fouˇ five six
5267 seven ˇten
5268 "});
5269}
5270
5271#[gpui::test]
5272fn test_delete_line(cx: &mut TestAppContext) {
5273 init_test(cx, |_| {});
5274
5275 let editor = cx.add_window(|window, cx| {
5276 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5277 build_editor(buffer, window, cx)
5278 });
5279 _ = editor.update(cx, |editor, window, cx| {
5280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5281 s.select_display_ranges([
5282 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5283 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5284 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5285 ])
5286 });
5287 editor.delete_line(&DeleteLine, window, cx);
5288 assert_eq!(editor.display_text(cx), "ghi");
5289 assert_eq!(
5290 display_ranges(editor, cx),
5291 vec![
5292 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
5293 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5294 ]
5295 );
5296 });
5297
5298 let editor = cx.add_window(|window, cx| {
5299 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5300 build_editor(buffer, window, cx)
5301 });
5302 _ = editor.update(cx, |editor, window, cx| {
5303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5304 s.select_display_ranges([
5305 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
5306 ])
5307 });
5308 editor.delete_line(&DeleteLine, window, cx);
5309 assert_eq!(editor.display_text(cx), "ghi\n");
5310 assert_eq!(
5311 display_ranges(editor, cx),
5312 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
5313 );
5314 });
5315
5316 let editor = cx.add_window(|window, cx| {
5317 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
5318 build_editor(buffer, window, cx)
5319 });
5320 _ = editor.update(cx, |editor, window, cx| {
5321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5322 s.select_display_ranges([
5323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
5324 ])
5325 });
5326 editor.delete_line(&DeleteLine, window, cx);
5327 assert_eq!(editor.display_text(cx), "\njkl\nmno");
5328 assert_eq!(
5329 display_ranges(editor, cx),
5330 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5331 );
5332 });
5333}
5334
5335#[gpui::test]
5336fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
5337 init_test(cx, |_| {});
5338
5339 cx.add_window(|window, cx| {
5340 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
5341 let mut editor = build_editor(buffer.clone(), window, cx);
5342 let buffer = buffer.read(cx).as_singleton().unwrap();
5343
5344 assert_eq!(
5345 editor
5346 .selections
5347 .ranges::<Point>(&editor.display_snapshot(cx)),
5348 &[Point::new(0, 0)..Point::new(0, 0)]
5349 );
5350
5351 // When on single line, replace newline at end by space
5352 editor.join_lines(&JoinLines, window, cx);
5353 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
5354 assert_eq!(
5355 editor
5356 .selections
5357 .ranges::<Point>(&editor.display_snapshot(cx)),
5358 &[Point::new(0, 3)..Point::new(0, 3)]
5359 );
5360
5361 editor.undo(&Undo, window, cx);
5362 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n\n");
5363
5364 // Select a full line, i.e. start of the first line to the start of the second line
5365 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5366 s.select_ranges([Point::new(0, 0)..Point::new(1, 0)])
5367 });
5368 editor.join_lines(&JoinLines, window, cx);
5369 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
5370
5371 editor.undo(&Undo, window, cx);
5372 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n\n");
5373
5374 // Select two full lines
5375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5376 s.select_ranges([Point::new(0, 0)..Point::new(2, 0)])
5377 });
5378 editor.join_lines(&JoinLines, window, cx);
5379
5380 // Only the selected lines should be joined, not the third.
5381 assert_eq!(
5382 buffer.read(cx).text(),
5383 "aaa bbb\nccc\nddd\n\n",
5384 "only the two selected lines (a and b) should be joined"
5385 );
5386
5387 // When multiple lines are selected, remove newlines that are spanned by the selection
5388 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5389 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
5390 });
5391 editor.join_lines(&JoinLines, window, cx);
5392 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
5393 assert_eq!(
5394 editor
5395 .selections
5396 .ranges::<Point>(&editor.display_snapshot(cx)),
5397 &[Point::new(0, 11)..Point::new(0, 11)]
5398 );
5399
5400 // Undo should be transactional
5401 editor.undo(&Undo, window, cx);
5402 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
5403 assert_eq!(
5404 editor
5405 .selections
5406 .ranges::<Point>(&editor.display_snapshot(cx)),
5407 &[Point::new(0, 5)..Point::new(2, 2)]
5408 );
5409
5410 // When joining an empty line don't insert a space
5411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5412 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
5413 });
5414 editor.join_lines(&JoinLines, window, cx);
5415 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
5416 assert_eq!(
5417 editor
5418 .selections
5419 .ranges::<Point>(&editor.display_snapshot(cx)),
5420 [Point::new(2, 3)..Point::new(2, 3)]
5421 );
5422
5423 // We can remove trailing newlines
5424 editor.join_lines(&JoinLines, window, cx);
5425 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
5426 assert_eq!(
5427 editor
5428 .selections
5429 .ranges::<Point>(&editor.display_snapshot(cx)),
5430 [Point::new(2, 3)..Point::new(2, 3)]
5431 );
5432
5433 // We don't blow up on the last line
5434 editor.join_lines(&JoinLines, window, cx);
5435 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
5436 assert_eq!(
5437 editor
5438 .selections
5439 .ranges::<Point>(&editor.display_snapshot(cx)),
5440 [Point::new(2, 3)..Point::new(2, 3)]
5441 );
5442
5443 // reset to test indentation
5444 editor.buffer.update(cx, |buffer, cx| {
5445 buffer.edit(
5446 [
5447 (Point::new(1, 0)..Point::new(1, 2), " "),
5448 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
5449 ],
5450 None,
5451 cx,
5452 )
5453 });
5454
5455 // We remove any leading spaces
5456 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
5457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5458 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
5459 });
5460 editor.join_lines(&JoinLines, window, cx);
5461 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
5462
5463 // We don't insert a space for a line containing only spaces
5464 editor.join_lines(&JoinLines, window, cx);
5465 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
5466
5467 // We ignore any leading tabs
5468 editor.join_lines(&JoinLines, window, cx);
5469 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
5470
5471 editor
5472 });
5473}
5474
5475#[gpui::test]
5476fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
5477 init_test(cx, |_| {});
5478
5479 cx.add_window(|window, cx| {
5480 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
5481 let mut editor = build_editor(buffer.clone(), window, cx);
5482 let buffer = buffer.read(cx).as_singleton().unwrap();
5483
5484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5485 s.select_ranges([
5486 Point::new(0, 2)..Point::new(1, 1),
5487 Point::new(1, 2)..Point::new(1, 2),
5488 Point::new(3, 1)..Point::new(3, 2),
5489 ])
5490 });
5491
5492 editor.join_lines(&JoinLines, window, cx);
5493 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
5494
5495 assert_eq!(
5496 editor
5497 .selections
5498 .ranges::<Point>(&editor.display_snapshot(cx)),
5499 [
5500 Point::new(0, 7)..Point::new(0, 7),
5501 Point::new(1, 3)..Point::new(1, 3)
5502 ]
5503 );
5504 editor
5505 });
5506}
5507
5508#[gpui::test]
5509async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
5510 init_test(cx, |_| {});
5511
5512 let mut cx = EditorTestContext::new(cx).await;
5513
5514 let diff_base = r#"
5515 Line 0
5516 Line 1
5517 Line 2
5518 Line 3
5519 "#
5520 .unindent();
5521
5522 cx.set_state(
5523 &r#"
5524 ˇLine 0
5525 Line 1
5526 Line 2
5527 Line 3
5528 "#
5529 .unindent(),
5530 );
5531
5532 cx.set_head_text(&diff_base);
5533 executor.run_until_parked();
5534
5535 // Join lines
5536 cx.update_editor(|editor, window, cx| {
5537 editor.join_lines(&JoinLines, window, cx);
5538 });
5539 executor.run_until_parked();
5540
5541 cx.assert_editor_state(
5542 &r#"
5543 Line 0ˇ Line 1
5544 Line 2
5545 Line 3
5546 "#
5547 .unindent(),
5548 );
5549 // Join again
5550 cx.update_editor(|editor, window, cx| {
5551 editor.join_lines(&JoinLines, window, cx);
5552 });
5553 executor.run_until_parked();
5554
5555 cx.assert_editor_state(
5556 &r#"
5557 Line 0 Line 1ˇ Line 2
5558 Line 3
5559 "#
5560 .unindent(),
5561 );
5562}
5563
5564#[gpui::test]
5565async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
5566 init_test(cx, |_| {});
5567
5568 {
5569 let language = Arc::new(Language::new(
5570 LanguageConfig {
5571 line_comments: vec!["// ".into(), "/// ".into()],
5572 documentation_comment: Some(BlockCommentConfig {
5573 start: "/*".into(),
5574 end: "*/".into(),
5575 prefix: "* ".into(),
5576 tab_size: 1,
5577 }),
5578 ..LanguageConfig::default()
5579 },
5580 None,
5581 ));
5582
5583 let mut cx = EditorTestContext::new(cx).await;
5584 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5585
5586 // Strips the comment prefix (with trailing space) from the joined-in line.
5587 cx.set_state(indoc! {"
5588 // ˇfoo
5589 // bar
5590 "});
5591 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5592 cx.assert_editor_state(indoc! {"
5593 // fooˇ bar
5594 "});
5595
5596 // Strips the longer doc-comment prefix when both `//` and `///` match.
5597 cx.set_state(indoc! {"
5598 /// ˇfoo
5599 /// bar
5600 "});
5601 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5602 cx.assert_editor_state(indoc! {"
5603 /// fooˇ bar
5604 "});
5605
5606 // Does not strip when the second line is a regular line (no comment prefix).
5607 cx.set_state(indoc! {"
5608 // ˇfoo
5609 bar
5610 "});
5611 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5612 cx.assert_editor_state(indoc! {"
5613 // fooˇ bar
5614 "});
5615
5616 // No-whitespace join also strips the comment prefix.
5617 cx.set_state(indoc! {"
5618 // ˇfoo
5619 // bar
5620 "});
5621 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5622 cx.assert_editor_state(indoc! {"
5623 // fooˇbar
5624 "});
5625
5626 // Strips even when the joined-in line is just the bare prefix (no trailing space).
5627 cx.set_state(indoc! {"
5628 // ˇfoo
5629 //
5630 "});
5631 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5632 cx.assert_editor_state(indoc! {"
5633 // fooˇ
5634 "});
5635
5636 // Mixed line comment prefix types: the longer matching prefix is stripped.
5637 cx.set_state(indoc! {"
5638 // ˇfoo
5639 /// bar
5640 "});
5641 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5642 cx.assert_editor_state(indoc! {"
5643 // fooˇ bar
5644 "});
5645
5646 // Strips block comment body prefix (`* `) from the joined-in line.
5647 cx.set_state(indoc! {"
5648 * ˇfoo
5649 * bar
5650 "});
5651 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5652 cx.assert_editor_state(indoc! {"
5653 * fooˇ bar
5654 "});
5655
5656 // Strips bare block comment body prefix (`*` without trailing space).
5657 cx.set_state(indoc! {"
5658 * ˇfoo
5659 *
5660 "});
5661 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5662 cx.assert_editor_state(indoc! {"
5663 * fooˇ
5664 "});
5665 }
5666
5667 {
5668 let markdown_language = Arc::new(Language::new(
5669 LanguageConfig {
5670 unordered_list: vec!["- ".into(), "* ".into(), "+ ".into()],
5671 ..LanguageConfig::default()
5672 },
5673 None,
5674 ));
5675
5676 let mut cx = EditorTestContext::new(cx).await;
5677 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
5678
5679 // Strips the `- ` list marker from the joined-in line.
5680 cx.set_state(indoc! {"
5681 - ˇfoo
5682 - bar
5683 "});
5684 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5685 cx.assert_editor_state(indoc! {"
5686 - fooˇ bar
5687 "});
5688
5689 // Strips the `* ` list marker from the joined-in line.
5690 cx.set_state(indoc! {"
5691 * ˇfoo
5692 * bar
5693 "});
5694 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5695 cx.assert_editor_state(indoc! {"
5696 * fooˇ bar
5697 "});
5698
5699 // Strips the `+ ` list marker from the joined-in line.
5700 cx.set_state(indoc! {"
5701 + ˇfoo
5702 + bar
5703 "});
5704 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5705 cx.assert_editor_state(indoc! {"
5706 + fooˇ bar
5707 "});
5708
5709 // No-whitespace join also strips the list marker.
5710 cx.set_state(indoc! {"
5711 - ˇfoo
5712 - bar
5713 "});
5714 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5715 cx.assert_editor_state(indoc! {"
5716 - fooˇbar
5717 "});
5718 }
5719}
5720
5721#[gpui::test]
5722async fn test_custom_newlines_cause_no_false_positive_diffs(
5723 executor: BackgroundExecutor,
5724 cx: &mut TestAppContext,
5725) {
5726 init_test(cx, |_| {});
5727 let mut cx = EditorTestContext::new(cx).await;
5728 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
5729 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
5730 executor.run_until_parked();
5731
5732 cx.update_editor(|editor, window, cx| {
5733 let snapshot = editor.snapshot(window, cx);
5734 assert_eq!(
5735 snapshot
5736 .buffer_snapshot()
5737 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5738 .collect::<Vec<_>>(),
5739 Vec::new(),
5740 "Should not have any diffs for files with custom newlines"
5741 );
5742 });
5743}
5744
5745#[gpui::test]
5746async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
5747 init_test(cx, |_| {});
5748
5749 let mut cx = EditorTestContext::new(cx).await;
5750
5751 // Test sort_lines_case_insensitive()
5752 cx.set_state(indoc! {"
5753 «z
5754 y
5755 x
5756 Z
5757 Y
5758 Xˇ»
5759 "});
5760 cx.update_editor(|e, window, cx| {
5761 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
5762 });
5763 cx.assert_editor_state(indoc! {"
5764 «x
5765 X
5766 y
5767 Y
5768 z
5769 Zˇ»
5770 "});
5771
5772 // Test sort_lines_by_length()
5773 //
5774 // Demonstrates:
5775 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
5776 // - sort is stable
5777 cx.set_state(indoc! {"
5778 «123
5779 æ
5780 12
5781 ∞
5782 1
5783 æˇ»
5784 "});
5785 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
5786 cx.assert_editor_state(indoc! {"
5787 «æ
5788 ∞
5789 1
5790 æ
5791 12
5792 123ˇ»
5793 "});
5794
5795 // Test reverse_lines()
5796 cx.set_state(indoc! {"
5797 «5
5798 4
5799 3
5800 2
5801 1ˇ»
5802 "});
5803 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
5804 cx.assert_editor_state(indoc! {"
5805 «1
5806 2
5807 3
5808 4
5809 5ˇ»
5810 "});
5811
5812 // Skip testing shuffle_line()
5813
5814 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
5815 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
5816
5817 // Don't manipulate when cursor is on single line, but expand the selection
5818 cx.set_state(indoc! {"
5819 ddˇdd
5820 ccc
5821 bb
5822 a
5823 "});
5824 cx.update_editor(|e, window, cx| {
5825 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5826 });
5827 cx.assert_editor_state(indoc! {"
5828 «ddddˇ»
5829 ccc
5830 bb
5831 a
5832 "});
5833
5834 // Basic manipulate case
5835 // Start selection moves to column 0
5836 // End of selection shrinks to fit shorter line
5837 cx.set_state(indoc! {"
5838 dd«d
5839 ccc
5840 bb
5841 aaaaaˇ»
5842 "});
5843 cx.update_editor(|e, window, cx| {
5844 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5845 });
5846 cx.assert_editor_state(indoc! {"
5847 «aaaaa
5848 bb
5849 ccc
5850 dddˇ»
5851 "});
5852
5853 // Manipulate case with newlines
5854 cx.set_state(indoc! {"
5855 dd«d
5856 ccc
5857
5858 bb
5859 aaaaa
5860
5861 ˇ»
5862 "});
5863 cx.update_editor(|e, window, cx| {
5864 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5865 });
5866 cx.assert_editor_state(indoc! {"
5867 «
5868
5869 aaaaa
5870 bb
5871 ccc
5872 dddˇ»
5873
5874 "});
5875
5876 // Adding new line
5877 cx.set_state(indoc! {"
5878 aa«a
5879 bbˇ»b
5880 "});
5881 cx.update_editor(|e, window, cx| {
5882 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
5883 });
5884 cx.assert_editor_state(indoc! {"
5885 «aaa
5886 bbb
5887 added_lineˇ»
5888 "});
5889
5890 // Removing line
5891 cx.set_state(indoc! {"
5892 aa«a
5893 bbbˇ»
5894 "});
5895 cx.update_editor(|e, window, cx| {
5896 e.manipulate_immutable_lines(window, cx, |lines| {
5897 lines.pop();
5898 })
5899 });
5900 cx.assert_editor_state(indoc! {"
5901 «aaaˇ»
5902 "});
5903
5904 // Removing all lines
5905 cx.set_state(indoc! {"
5906 aa«a
5907 bbbˇ»
5908 "});
5909 cx.update_editor(|e, window, cx| {
5910 e.manipulate_immutable_lines(window, cx, |lines| {
5911 lines.drain(..);
5912 })
5913 });
5914 cx.assert_editor_state(indoc! {"
5915 ˇ
5916 "});
5917}
5918
5919#[gpui::test]
5920async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5921 init_test(cx, |_| {});
5922
5923 let mut cx = EditorTestContext::new(cx).await;
5924
5925 // Consider continuous selection as single selection
5926 cx.set_state(indoc! {"
5927 Aaa«aa
5928 cˇ»c«c
5929 bb
5930 aaaˇ»aa
5931 "});
5932 cx.update_editor(|e, window, cx| {
5933 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5934 });
5935 cx.assert_editor_state(indoc! {"
5936 «Aaaaa
5937 ccc
5938 bb
5939 aaaaaˇ»
5940 "});
5941
5942 cx.set_state(indoc! {"
5943 Aaa«aa
5944 cˇ»c«c
5945 bb
5946 aaaˇ»aa
5947 "});
5948 cx.update_editor(|e, window, cx| {
5949 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5950 });
5951 cx.assert_editor_state(indoc! {"
5952 «Aaaaa
5953 ccc
5954 bbˇ»
5955 "});
5956
5957 // Consider non continuous selection as distinct dedup operations
5958 cx.set_state(indoc! {"
5959 «aaaaa
5960 bb
5961 aaaaa
5962 aaaaaˇ»
5963
5964 aaa«aaˇ»
5965 "});
5966 cx.update_editor(|e, window, cx| {
5967 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5968 });
5969 cx.assert_editor_state(indoc! {"
5970 «aaaaa
5971 bbˇ»
5972
5973 «aaaaaˇ»
5974 "});
5975}
5976
5977#[gpui::test]
5978async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5979 init_test(cx, |_| {});
5980
5981 let mut cx = EditorTestContext::new(cx).await;
5982
5983 cx.set_state(indoc! {"
5984 «Aaa
5985 aAa
5986 Aaaˇ»
5987 "});
5988 cx.update_editor(|e, window, cx| {
5989 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5990 });
5991 cx.assert_editor_state(indoc! {"
5992 «Aaa
5993 aAaˇ»
5994 "});
5995
5996 cx.set_state(indoc! {"
5997 «Aaa
5998 aAa
5999 aaAˇ»
6000 "});
6001 cx.update_editor(|e, window, cx| {
6002 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
6003 });
6004 cx.assert_editor_state(indoc! {"
6005 «Aaaˇ»
6006 "});
6007}
6008
6009#[gpui::test]
6010async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
6011 init_test(cx, |_| {});
6012
6013 let mut cx = EditorTestContext::new(cx).await;
6014
6015 let js_language = Arc::new(Language::new(
6016 LanguageConfig {
6017 name: "JavaScript".into(),
6018 wrap_characters: Some(language::WrapCharactersConfig {
6019 start_prefix: "<".into(),
6020 start_suffix: ">".into(),
6021 end_prefix: "</".into(),
6022 end_suffix: ">".into(),
6023 }),
6024 ..LanguageConfig::default()
6025 },
6026 None,
6027 ));
6028
6029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
6030
6031 cx.set_state(indoc! {"
6032 «testˇ»
6033 "});
6034 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6035 cx.assert_editor_state(indoc! {"
6036 <«ˇ»>test</«ˇ»>
6037 "});
6038
6039 cx.set_state(indoc! {"
6040 «test
6041 testˇ»
6042 "});
6043 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6044 cx.assert_editor_state(indoc! {"
6045 <«ˇ»>test
6046 test</«ˇ»>
6047 "});
6048
6049 cx.set_state(indoc! {"
6050 teˇst
6051 "});
6052 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6053 cx.assert_editor_state(indoc! {"
6054 te<«ˇ»></«ˇ»>st
6055 "});
6056}
6057
6058#[gpui::test]
6059async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
6060 init_test(cx, |_| {});
6061
6062 let mut cx = EditorTestContext::new(cx).await;
6063
6064 let js_language = Arc::new(Language::new(
6065 LanguageConfig {
6066 name: "JavaScript".into(),
6067 wrap_characters: Some(language::WrapCharactersConfig {
6068 start_prefix: "<".into(),
6069 start_suffix: ">".into(),
6070 end_prefix: "</".into(),
6071 end_suffix: ">".into(),
6072 }),
6073 ..LanguageConfig::default()
6074 },
6075 None,
6076 ));
6077
6078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
6079
6080 cx.set_state(indoc! {"
6081 «testˇ»
6082 «testˇ» «testˇ»
6083 «testˇ»
6084 "});
6085 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6086 cx.assert_editor_state(indoc! {"
6087 <«ˇ»>test</«ˇ»>
6088 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
6089 <«ˇ»>test</«ˇ»>
6090 "});
6091
6092 cx.set_state(indoc! {"
6093 «test
6094 testˇ»
6095 «test
6096 testˇ»
6097 "});
6098 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6099 cx.assert_editor_state(indoc! {"
6100 <«ˇ»>test
6101 test</«ˇ»>
6102 <«ˇ»>test
6103 test</«ˇ»>
6104 "});
6105}
6106
6107#[gpui::test]
6108async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
6109 init_test(cx, |_| {});
6110
6111 let mut cx = EditorTestContext::new(cx).await;
6112
6113 let plaintext_language = Arc::new(Language::new(
6114 LanguageConfig {
6115 name: "Plain Text".into(),
6116 ..LanguageConfig::default()
6117 },
6118 None,
6119 ));
6120
6121 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
6122
6123 cx.set_state(indoc! {"
6124 «testˇ»
6125 "});
6126 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
6127 cx.assert_editor_state(indoc! {"
6128 «testˇ»
6129 "});
6130}
6131
6132#[gpui::test]
6133async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
6134 init_test(cx, |_| {});
6135
6136 let mut cx = EditorTestContext::new(cx).await;
6137
6138 // Manipulate with multiple selections on a single line
6139 cx.set_state(indoc! {"
6140 dd«dd
6141 cˇ»c«c
6142 bb
6143 aaaˇ»aa
6144 "});
6145 cx.update_editor(|e, window, cx| {
6146 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
6147 });
6148 cx.assert_editor_state(indoc! {"
6149 «aaaaa
6150 bb
6151 ccc
6152 ddddˇ»
6153 "});
6154
6155 // Manipulate with multiple disjoin selections
6156 cx.set_state(indoc! {"
6157 5«
6158 4
6159 3
6160 2
6161 1ˇ»
6162
6163 dd«dd
6164 ccc
6165 bb
6166 aaaˇ»aa
6167 "});
6168 cx.update_editor(|e, window, cx| {
6169 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
6170 });
6171 cx.assert_editor_state(indoc! {"
6172 «1
6173 2
6174 3
6175 4
6176 5ˇ»
6177
6178 «aaaaa
6179 bb
6180 ccc
6181 ddddˇ»
6182 "});
6183
6184 // Adding lines on each selection
6185 cx.set_state(indoc! {"
6186 2«
6187 1ˇ»
6188
6189 bb«bb
6190 aaaˇ»aa
6191 "});
6192 cx.update_editor(|e, window, cx| {
6193 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
6194 });
6195 cx.assert_editor_state(indoc! {"
6196 «2
6197 1
6198 added lineˇ»
6199
6200 «bbbb
6201 aaaaa
6202 added lineˇ»
6203 "});
6204
6205 // Removing lines on each selection
6206 cx.set_state(indoc! {"
6207 2«
6208 1ˇ»
6209
6210 bb«bb
6211 aaaˇ»aa
6212 "});
6213 cx.update_editor(|e, window, cx| {
6214 e.manipulate_immutable_lines(window, cx, |lines| {
6215 lines.pop();
6216 })
6217 });
6218 cx.assert_editor_state(indoc! {"
6219 «2ˇ»
6220
6221 «bbbbˇ»
6222 "});
6223}
6224
6225#[gpui::test]
6226async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
6227 init_test(cx, |settings| {
6228 settings.defaults.tab_size = NonZeroU32::new(3)
6229 });
6230
6231 let mut cx = EditorTestContext::new(cx).await;
6232
6233 // MULTI SELECTION
6234 // Ln.1 "«" tests empty lines
6235 // Ln.9 tests just leading whitespace
6236 cx.set_state(indoc! {"
6237 «
6238 abc // No indentationˇ»
6239 «\tabc // 1 tabˇ»
6240 \t\tabc « ˇ» // 2 tabs
6241 \t ab«c // Tab followed by space
6242 \tabc // Space followed by tab (3 spaces should be the result)
6243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
6244 abˇ»ˇc ˇ ˇ // Already space indented«
6245 \t
6246 \tabc\tdef // Only the leading tab is manipulatedˇ»
6247 "});
6248 cx.update_editor(|e, window, cx| {
6249 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
6250 });
6251 cx.assert_editor_state(
6252 indoc! {"
6253 «
6254 abc // No indentation
6255 abc // 1 tab
6256 abc // 2 tabs
6257 abc // Tab followed by space
6258 abc // Space followed by tab (3 spaces should be the result)
6259 abc // Mixed indentation (tab conversion depends on the column)
6260 abc // Already space indented
6261 ·
6262 abc\tdef // Only the leading tab is manipulatedˇ»
6263 "}
6264 .replace("·", "")
6265 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6266 );
6267
6268 // Test on just a few lines, the others should remain unchanged
6269 // Only lines (3, 5, 10, 11) should change
6270 cx.set_state(
6271 indoc! {"
6272 ·
6273 abc // No indentation
6274 \tabcˇ // 1 tab
6275 \t\tabc // 2 tabs
6276 \t abcˇ // Tab followed by space
6277 \tabc // Space followed by tab (3 spaces should be the result)
6278 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
6279 abc // Already space indented
6280 «\t
6281 \tabc\tdef // Only the leading tab is manipulatedˇ»
6282 "}
6283 .replace("·", "")
6284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6285 );
6286 cx.update_editor(|e, window, cx| {
6287 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
6288 });
6289 cx.assert_editor_state(
6290 indoc! {"
6291 ·
6292 abc // No indentation
6293 « abc // 1 tabˇ»
6294 \t\tabc // 2 tabs
6295 « abc // Tab followed by spaceˇ»
6296 \tabc // Space followed by tab (3 spaces should be the result)
6297 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
6298 abc // Already space indented
6299 « ·
6300 abc\tdef // Only the leading tab is manipulatedˇ»
6301 "}
6302 .replace("·", "")
6303 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6304 );
6305
6306 // SINGLE SELECTION
6307 // Ln.1 "«" tests empty lines
6308 // Ln.9 tests just leading whitespace
6309 cx.set_state(indoc! {"
6310 «
6311 abc // No indentation
6312 \tabc // 1 tab
6313 \t\tabc // 2 tabs
6314 \t abc // Tab followed by space
6315 \tabc // Space followed by tab (3 spaces should be the result)
6316 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
6317 abc // Already space indented
6318 \t
6319 \tabc\tdef // Only the leading tab is manipulatedˇ»
6320 "});
6321 cx.update_editor(|e, window, cx| {
6322 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
6323 });
6324 cx.assert_editor_state(
6325 indoc! {"
6326 «
6327 abc // No indentation
6328 abc // 1 tab
6329 abc // 2 tabs
6330 abc // Tab followed by space
6331 abc // Space followed by tab (3 spaces should be the result)
6332 abc // Mixed indentation (tab conversion depends on the column)
6333 abc // Already space indented
6334 ·
6335 abc\tdef // Only the leading tab is manipulatedˇ»
6336 "}
6337 .replace("·", "")
6338 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6339 );
6340}
6341
6342#[gpui::test]
6343async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
6344 init_test(cx, |settings| {
6345 settings.defaults.tab_size = NonZeroU32::new(3)
6346 });
6347
6348 let mut cx = EditorTestContext::new(cx).await;
6349
6350 // MULTI SELECTION
6351 // Ln.1 "«" tests empty lines
6352 // Ln.11 tests just leading whitespace
6353 cx.set_state(indoc! {"
6354 «
6355 abˇ»ˇc // No indentation
6356 abc ˇ ˇ // 1 space (< 3 so dont convert)
6357 abc « // 2 spaces (< 3 so dont convert)
6358 abc // 3 spaces (convert)
6359 abc ˇ» // 5 spaces (1 tab + 2 spaces)
6360 «\tˇ»\t«\tˇ»abc // Already tab indented
6361 «\t abc // Tab followed by space
6362 \tabc // Space followed by tab (should be consumed due to tab)
6363 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6364 \tˇ» «\t
6365 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
6366 "});
6367 cx.update_editor(|e, window, cx| {
6368 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6369 });
6370 cx.assert_editor_state(indoc! {"
6371 «
6372 abc // No indentation
6373 abc // 1 space (< 3 so dont convert)
6374 abc // 2 spaces (< 3 so dont convert)
6375 \tabc // 3 spaces (convert)
6376 \t abc // 5 spaces (1 tab + 2 spaces)
6377 \t\t\tabc // Already tab indented
6378 \t abc // Tab followed by space
6379 \tabc // Space followed by tab (should be consumed due to tab)
6380 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6381 \t\t\t
6382 \tabc \t // Only the leading spaces should be convertedˇ»
6383 "});
6384
6385 // Test on just a few lines, the other should remain unchanged
6386 // Only lines (4, 8, 11, 12) should change
6387 cx.set_state(
6388 indoc! {"
6389 ·
6390 abc // No indentation
6391 abc // 1 space (< 3 so dont convert)
6392 abc // 2 spaces (< 3 so dont convert)
6393 « abc // 3 spaces (convert)ˇ»
6394 abc // 5 spaces (1 tab + 2 spaces)
6395 \t\t\tabc // Already tab indented
6396 \t abc // Tab followed by space
6397 \tabc ˇ // Space followed by tab (should be consumed due to tab)
6398 \t\t \tabc // Mixed indentation
6399 \t \t \t \tabc // Mixed indentation
6400 \t \tˇ
6401 « abc \t // Only the leading spaces should be convertedˇ»
6402 "}
6403 .replace("·", "")
6404 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6405 );
6406 cx.update_editor(|e, window, cx| {
6407 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6408 });
6409 cx.assert_editor_state(
6410 indoc! {"
6411 ·
6412 abc // No indentation
6413 abc // 1 space (< 3 so dont convert)
6414 abc // 2 spaces (< 3 so dont convert)
6415 «\tabc // 3 spaces (convert)ˇ»
6416 abc // 5 spaces (1 tab + 2 spaces)
6417 \t\t\tabc // Already tab indented
6418 \t abc // Tab followed by space
6419 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
6420 \t\t \tabc // Mixed indentation
6421 \t \t \t \tabc // Mixed indentation
6422 «\t\t\t
6423 \tabc \t // Only the leading spaces should be convertedˇ»
6424 "}
6425 .replace("·", "")
6426 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6427 );
6428
6429 // SINGLE SELECTION
6430 // Ln.1 "«" tests empty lines
6431 // Ln.11 tests just leading whitespace
6432 cx.set_state(indoc! {"
6433 «
6434 abc // No indentation
6435 abc // 1 space (< 3 so dont convert)
6436 abc // 2 spaces (< 3 so dont convert)
6437 abc // 3 spaces (convert)
6438 abc // 5 spaces (1 tab + 2 spaces)
6439 \t\t\tabc // Already tab indented
6440 \t abc // Tab followed by space
6441 \tabc // Space followed by tab (should be consumed due to tab)
6442 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6443 \t \t
6444 abc \t // Only the leading spaces should be convertedˇ»
6445 "});
6446 cx.update_editor(|e, window, cx| {
6447 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6448 });
6449 cx.assert_editor_state(indoc! {"
6450 «
6451 abc // No indentation
6452 abc // 1 space (< 3 so dont convert)
6453 abc // 2 spaces (< 3 so dont convert)
6454 \tabc // 3 spaces (convert)
6455 \t abc // 5 spaces (1 tab + 2 spaces)
6456 \t\t\tabc // Already tab indented
6457 \t abc // Tab followed by space
6458 \tabc // Space followed by tab (should be consumed due to tab)
6459 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6460 \t\t\t
6461 \tabc \t // Only the leading spaces should be convertedˇ»
6462 "});
6463}
6464
6465#[gpui::test]
6466async fn test_toggle_case(cx: &mut TestAppContext) {
6467 init_test(cx, |_| {});
6468
6469 let mut cx = EditorTestContext::new(cx).await;
6470
6471 // If all lower case -> upper case
6472 cx.set_state(indoc! {"
6473 «hello worldˇ»
6474 "});
6475 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6476 cx.assert_editor_state(indoc! {"
6477 «HELLO WORLDˇ»
6478 "});
6479
6480 // If all upper case -> lower case
6481 cx.set_state(indoc! {"
6482 «HELLO WORLDˇ»
6483 "});
6484 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6485 cx.assert_editor_state(indoc! {"
6486 «hello worldˇ»
6487 "});
6488
6489 // If any upper case characters are identified -> lower case
6490 // This matches JetBrains IDEs
6491 cx.set_state(indoc! {"
6492 «hEllo worldˇ»
6493 "});
6494 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6495 cx.assert_editor_state(indoc! {"
6496 «hello worldˇ»
6497 "});
6498}
6499
6500#[gpui::test]
6501async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
6502 init_test(cx, |_| {});
6503
6504 let mut cx = EditorTestContext::new(cx).await;
6505
6506 cx.set_state(indoc! {"
6507 «implement-windows-supportˇ»
6508 "});
6509 cx.update_editor(|e, window, cx| {
6510 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
6511 });
6512 cx.assert_editor_state(indoc! {"
6513 «Implement windows supportˇ»
6514 "});
6515}
6516
6517#[gpui::test]
6518async fn test_manipulate_text(cx: &mut TestAppContext) {
6519 init_test(cx, |_| {});
6520
6521 let mut cx = EditorTestContext::new(cx).await;
6522
6523 // Test convert_to_upper_case()
6524 cx.set_state(indoc! {"
6525 «hello worldˇ»
6526 "});
6527 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6528 cx.assert_editor_state(indoc! {"
6529 «HELLO WORLDˇ»
6530 "});
6531
6532 // Test convert_to_lower_case()
6533 cx.set_state(indoc! {"
6534 «HELLO WORLDˇ»
6535 "});
6536 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
6537 cx.assert_editor_state(indoc! {"
6538 «hello worldˇ»
6539 "});
6540
6541 // Test multiple line, single selection case
6542 cx.set_state(indoc! {"
6543 «The quick brown
6544 fox jumps over
6545 the lazy dogˇ»
6546 "});
6547 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6548 cx.assert_editor_state(indoc! {"
6549 «The Quick Brown
6550 Fox Jumps Over
6551 The Lazy Dogˇ»
6552 "});
6553
6554 // Test multiple line, single selection case
6555 cx.set_state(indoc! {"
6556 «The quick brown
6557 fox jumps over
6558 the lazy dogˇ»
6559 "});
6560 cx.update_editor(|e, window, cx| {
6561 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
6562 });
6563 cx.assert_editor_state(indoc! {"
6564 «TheQuickBrown
6565 FoxJumpsOver
6566 TheLazyDogˇ»
6567 "});
6568
6569 // From here on out, test more complex cases of manipulate_text()
6570
6571 // Test no selection case - should affect words cursors are in
6572 // Cursor at beginning, middle, and end of word
6573 cx.set_state(indoc! {"
6574 ˇhello big beauˇtiful worldˇ
6575 "});
6576 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6577 cx.assert_editor_state(indoc! {"
6578 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
6579 "});
6580
6581 // Test multiple selections on a single line and across multiple lines
6582 cx.set_state(indoc! {"
6583 «Theˇ» quick «brown
6584 foxˇ» jumps «overˇ»
6585 the «lazyˇ» dog
6586 "});
6587 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6588 cx.assert_editor_state(indoc! {"
6589 «THEˇ» quick «BROWN
6590 FOXˇ» jumps «OVERˇ»
6591 the «LAZYˇ» dog
6592 "});
6593
6594 // Test case where text length grows
6595 cx.set_state(indoc! {"
6596 «tschüߡ»
6597 "});
6598 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6599 cx.assert_editor_state(indoc! {"
6600 «TSCHÜSSˇ»
6601 "});
6602
6603 // Test to make sure we don't crash when text shrinks
6604 cx.set_state(indoc! {"
6605 aaa_bbbˇ
6606 "});
6607 cx.update_editor(|e, window, cx| {
6608 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6609 });
6610 cx.assert_editor_state(indoc! {"
6611 «aaaBbbˇ»
6612 "});
6613
6614 // Test to make sure we all aware of the fact that each word can grow and shrink
6615 // Final selections should be aware of this fact
6616 cx.set_state(indoc! {"
6617 aaa_bˇbb bbˇb_ccc ˇccc_ddd
6618 "});
6619 cx.update_editor(|e, window, cx| {
6620 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6621 });
6622 cx.assert_editor_state(indoc! {"
6623 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
6624 "});
6625
6626 cx.set_state(indoc! {"
6627 «hElLo, WoRld!ˇ»
6628 "});
6629 cx.update_editor(|e, window, cx| {
6630 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
6631 });
6632 cx.assert_editor_state(indoc! {"
6633 «HeLlO, wOrLD!ˇ»
6634 "});
6635
6636 // Test that case conversions backed by `to_case` preserve leading/trailing whitespace.
6637 cx.set_state(indoc! {"
6638 « hello worldˇ»
6639 "});
6640 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6641 cx.assert_editor_state(indoc! {"
6642 « Hello Worldˇ»
6643 "});
6644
6645 cx.set_state(indoc! {"
6646 « hello worldˇ»
6647 "});
6648 cx.update_editor(|e, window, cx| {
6649 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
6650 });
6651 cx.assert_editor_state(indoc! {"
6652 « HelloWorldˇ»
6653 "});
6654
6655 cx.set_state(indoc! {"
6656 « hello worldˇ»
6657 "});
6658 cx.update_editor(|e, window, cx| {
6659 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6660 });
6661 cx.assert_editor_state(indoc! {"
6662 « helloWorldˇ»
6663 "});
6664
6665 cx.set_state(indoc! {"
6666 « hello worldˇ»
6667 "});
6668 cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
6669 cx.assert_editor_state(indoc! {"
6670 « hello_worldˇ»
6671 "});
6672
6673 cx.set_state(indoc! {"
6674 « hello worldˇ»
6675 "});
6676 cx.update_editor(|e, window, cx| e.convert_to_kebab_case(&ConvertToKebabCase, window, cx));
6677 cx.assert_editor_state(indoc! {"
6678 « hello-worldˇ»
6679 "});
6680
6681 cx.set_state(indoc! {"
6682 « hello worldˇ»
6683 "});
6684 cx.update_editor(|e, window, cx| {
6685 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
6686 });
6687 cx.assert_editor_state(indoc! {"
6688 « Hello worldˇ»
6689 "});
6690
6691 cx.set_state(indoc! {"
6692 « hello world\t\tˇ»
6693 "});
6694 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6695 cx.assert_editor_state(indoc! {"
6696 « Hello World\t\tˇ»
6697 "});
6698
6699 cx.set_state(indoc! {"
6700 « hello world\t\tˇ»
6701 "});
6702 cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
6703 cx.assert_editor_state(indoc! {"
6704 « hello_world\t\tˇ»
6705 "});
6706
6707 // Test selections with `line_mode() = true`.
6708 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
6709 cx.set_state(indoc! {"
6710 «The quick brown
6711 fox jumps over
6712 tˇ»he lazy dog
6713 "});
6714 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6715 cx.assert_editor_state(indoc! {"
6716 «THE QUICK BROWN
6717 FOX JUMPS OVER
6718 THE LAZY DOGˇ»
6719 "});
6720}
6721
6722#[gpui::test]
6723fn test_duplicate_line(cx: &mut TestAppContext) {
6724 init_test(cx, |_| {});
6725
6726 let editor = cx.add_window(|window, cx| {
6727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6728 build_editor(buffer, window, cx)
6729 });
6730 _ = editor.update(cx, |editor, window, cx| {
6731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6732 s.select_display_ranges([
6733 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6734 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6735 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6736 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6737 ])
6738 });
6739 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6740 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6741 assert_eq!(
6742 display_ranges(editor, cx),
6743 vec![
6744 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6745 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
6746 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6747 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
6748 ]
6749 );
6750 });
6751
6752 let editor = cx.add_window(|window, cx| {
6753 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6754 build_editor(buffer, window, cx)
6755 });
6756 _ = editor.update(cx, |editor, window, cx| {
6757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6758 s.select_display_ranges([
6759 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6760 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6761 ])
6762 });
6763 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6764 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6765 assert_eq!(
6766 display_ranges(editor, cx),
6767 vec![
6768 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
6769 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
6770 ]
6771 );
6772 });
6773
6774 // With `duplicate_line_up` the selections move to the duplicated lines,
6775 // which are inserted above the original lines
6776 let editor = cx.add_window(|window, cx| {
6777 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6778 build_editor(buffer, window, cx)
6779 });
6780 _ = editor.update(cx, |editor, window, cx| {
6781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6782 s.select_display_ranges([
6783 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6784 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6785 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6786 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6787 ])
6788 });
6789 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6790 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6791 assert_eq!(
6792 display_ranges(editor, cx),
6793 vec![
6794 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6796 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
6797 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
6798 ]
6799 );
6800 });
6801
6802 let editor = cx.add_window(|window, cx| {
6803 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6804 build_editor(buffer, window, cx)
6805 });
6806 _ = editor.update(cx, |editor, window, cx| {
6807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6808 s.select_display_ranges([
6809 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6810 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6811 ])
6812 });
6813 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6814 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6815 assert_eq!(
6816 display_ranges(editor, cx),
6817 vec![
6818 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6819 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6820 ]
6821 );
6822 });
6823
6824 let editor = cx.add_window(|window, cx| {
6825 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6826 build_editor(buffer, window, cx)
6827 });
6828 _ = editor.update(cx, |editor, window, cx| {
6829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6830 s.select_display_ranges([
6831 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6832 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6833 ])
6834 });
6835 editor.duplicate_selection(&DuplicateSelection, window, cx);
6836 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
6837 assert_eq!(
6838 display_ranges(editor, cx),
6839 vec![
6840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6841 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
6842 ]
6843 );
6844 });
6845}
6846
6847#[gpui::test]
6848async fn test_rotate_selections(cx: &mut TestAppContext) {
6849 init_test(cx, |_| {});
6850
6851 let mut cx = EditorTestContext::new(cx).await;
6852
6853 // Rotate text selections (horizontal)
6854 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6855 cx.update_editor(|e, window, cx| {
6856 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6857 });
6858 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
6859 cx.update_editor(|e, window, cx| {
6860 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6861 });
6862 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6863
6864 // Rotate text selections (vertical)
6865 cx.set_state(indoc! {"
6866 x=«1ˇ»
6867 y=«2ˇ»
6868 z=«3ˇ»
6869 "});
6870 cx.update_editor(|e, window, cx| {
6871 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6872 });
6873 cx.assert_editor_state(indoc! {"
6874 x=«3ˇ»
6875 y=«1ˇ»
6876 z=«2ˇ»
6877 "});
6878 cx.update_editor(|e, window, cx| {
6879 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6880 });
6881 cx.assert_editor_state(indoc! {"
6882 x=«1ˇ»
6883 y=«2ˇ»
6884 z=«3ˇ»
6885 "});
6886
6887 // Rotate text selections (vertical, different lengths)
6888 cx.set_state(indoc! {"
6889 x=\"«ˇ»\"
6890 y=\"«aˇ»\"
6891 z=\"«aaˇ»\"
6892 "});
6893 cx.update_editor(|e, window, cx| {
6894 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6895 });
6896 cx.assert_editor_state(indoc! {"
6897 x=\"«aaˇ»\"
6898 y=\"«ˇ»\"
6899 z=\"«aˇ»\"
6900 "});
6901 cx.update_editor(|e, window, cx| {
6902 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6903 });
6904 cx.assert_editor_state(indoc! {"
6905 x=\"«ˇ»\"
6906 y=\"«aˇ»\"
6907 z=\"«aaˇ»\"
6908 "});
6909
6910 // Rotate whole lines (cursor positions preserved)
6911 cx.set_state(indoc! {"
6912 ˇline123
6913 liˇne23
6914 line3ˇ
6915 "});
6916 cx.update_editor(|e, window, cx| {
6917 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6918 });
6919 cx.assert_editor_state(indoc! {"
6920 line3ˇ
6921 ˇline123
6922 liˇne23
6923 "});
6924 cx.update_editor(|e, window, cx| {
6925 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6926 });
6927 cx.assert_editor_state(indoc! {"
6928 ˇline123
6929 liˇne23
6930 line3ˇ
6931 "});
6932
6933 // Rotate whole lines, multiple cursors per line (positions preserved)
6934 cx.set_state(indoc! {"
6935 ˇliˇne123
6936 ˇline23
6937 ˇline3
6938 "});
6939 cx.update_editor(|e, window, cx| {
6940 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6941 });
6942 cx.assert_editor_state(indoc! {"
6943 ˇline3
6944 ˇliˇne123
6945 ˇline23
6946 "});
6947 cx.update_editor(|e, window, cx| {
6948 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6949 });
6950 cx.assert_editor_state(indoc! {"
6951 ˇliˇne123
6952 ˇline23
6953 ˇline3
6954 "});
6955}
6956
6957#[gpui::test]
6958fn test_move_line_up_down(cx: &mut TestAppContext) {
6959 init_test(cx, |_| {});
6960
6961 let editor = cx.add_window(|window, cx| {
6962 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6963 build_editor(buffer, window, cx)
6964 });
6965 _ = editor.update(cx, |editor, window, cx| {
6966 editor.fold_creases(
6967 vec![
6968 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6969 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6970 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6971 ],
6972 true,
6973 window,
6974 cx,
6975 );
6976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6977 s.select_display_ranges([
6978 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6979 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6980 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6981 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
6982 ])
6983 });
6984 assert_eq!(
6985 editor.display_text(cx),
6986 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6987 );
6988
6989 editor.move_line_up(&MoveLineUp, window, cx);
6990 assert_eq!(
6991 editor.display_text(cx),
6992 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6993 );
6994 assert_eq!(
6995 display_ranges(editor, cx),
6996 vec![
6997 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6998 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6999 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
7000 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
7001 ]
7002 );
7003 });
7004
7005 _ = editor.update(cx, |editor, window, cx| {
7006 editor.move_line_down(&MoveLineDown, window, cx);
7007 assert_eq!(
7008 editor.display_text(cx),
7009 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
7010 );
7011 assert_eq!(
7012 display_ranges(editor, cx),
7013 vec![
7014 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7015 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
7016 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
7017 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
7018 ]
7019 );
7020 });
7021
7022 _ = editor.update(cx, |editor, window, cx| {
7023 editor.move_line_down(&MoveLineDown, window, cx);
7024 assert_eq!(
7025 editor.display_text(cx),
7026 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
7027 );
7028 assert_eq!(
7029 display_ranges(editor, cx),
7030 vec![
7031 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
7032 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
7033 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
7034 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
7035 ]
7036 );
7037 });
7038
7039 _ = editor.update(cx, |editor, window, cx| {
7040 editor.move_line_up(&MoveLineUp, window, cx);
7041 assert_eq!(
7042 editor.display_text(cx),
7043 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
7044 );
7045 assert_eq!(
7046 display_ranges(editor, cx),
7047 vec![
7048 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7049 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
7050 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
7051 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
7052 ]
7053 );
7054 });
7055}
7056
7057#[gpui::test]
7058fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
7059 init_test(cx, |_| {});
7060 let editor = cx.add_window(|window, cx| {
7061 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
7062 build_editor(buffer, window, cx)
7063 });
7064 _ = editor.update(cx, |editor, window, cx| {
7065 editor.fold_creases(
7066 vec![Crease::simple(
7067 Point::new(6, 4)..Point::new(7, 4),
7068 FoldPlaceholder::test(),
7069 )],
7070 true,
7071 window,
7072 cx,
7073 );
7074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7075 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
7076 });
7077 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
7078 editor.move_line_up(&MoveLineUp, window, cx);
7079 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
7080 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
7081 });
7082}
7083
7084#[gpui::test]
7085fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
7086 init_test(cx, |_| {});
7087
7088 let editor = cx.add_window(|window, cx| {
7089 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
7090 build_editor(buffer, window, cx)
7091 });
7092 _ = editor.update(cx, |editor, window, cx| {
7093 let snapshot = editor.buffer.read(cx).snapshot(cx);
7094 editor.insert_blocks(
7095 [BlockProperties {
7096 style: BlockStyle::Fixed,
7097 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
7098 height: Some(1),
7099 render: Arc::new(|_| div().into_any()),
7100 priority: 0,
7101 }],
7102 Some(Autoscroll::fit()),
7103 cx,
7104 );
7105 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7106 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
7107 });
7108 editor.move_line_down(&MoveLineDown, window, cx);
7109 });
7110}
7111
7112#[gpui::test]
7113async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
7114 init_test(cx, |_| {});
7115
7116 let mut cx = EditorTestContext::new(cx).await;
7117 cx.set_state(
7118 &"
7119 ˇzero
7120 one
7121 two
7122 three
7123 four
7124 five
7125 "
7126 .unindent(),
7127 );
7128
7129 // Create a four-line block that replaces three lines of text.
7130 cx.update_editor(|editor, window, cx| {
7131 let snapshot = editor.snapshot(window, cx);
7132 let snapshot = &snapshot.buffer_snapshot();
7133 let placement = BlockPlacement::Replace(
7134 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
7135 );
7136 editor.insert_blocks(
7137 [BlockProperties {
7138 placement,
7139 height: Some(4),
7140 style: BlockStyle::Sticky,
7141 render: Arc::new(|_| gpui::div().into_any_element()),
7142 priority: 0,
7143 }],
7144 None,
7145 cx,
7146 );
7147 });
7148
7149 // Move down so that the cursor touches the block.
7150 cx.update_editor(|editor, window, cx| {
7151 editor.move_down(&Default::default(), window, cx);
7152 });
7153 cx.assert_editor_state(
7154 &"
7155 zero
7156 «one
7157 two
7158 threeˇ»
7159 four
7160 five
7161 "
7162 .unindent(),
7163 );
7164
7165 // Move down past the block.
7166 cx.update_editor(|editor, window, cx| {
7167 editor.move_down(&Default::default(), window, cx);
7168 });
7169 cx.assert_editor_state(
7170 &"
7171 zero
7172 one
7173 two
7174 three
7175 ˇfour
7176 five
7177 "
7178 .unindent(),
7179 );
7180}
7181
7182#[gpui::test]
7183fn test_transpose(cx: &mut TestAppContext) {
7184 init_test(cx, |_| {});
7185
7186 _ = cx.add_window(|window, cx| {
7187 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
7188 editor.set_style(EditorStyle::default(), window, cx);
7189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7190 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
7191 });
7192 editor.transpose(&Default::default(), window, cx);
7193 assert_eq!(editor.text(cx), "bac");
7194 assert_eq!(
7195 editor.selections.ranges(&editor.display_snapshot(cx)),
7196 [MultiBufferOffset(2)..MultiBufferOffset(2)]
7197 );
7198
7199 editor.transpose(&Default::default(), window, cx);
7200 assert_eq!(editor.text(cx), "bca");
7201 assert_eq!(
7202 editor.selections.ranges(&editor.display_snapshot(cx)),
7203 [MultiBufferOffset(3)..MultiBufferOffset(3)]
7204 );
7205
7206 editor.transpose(&Default::default(), window, cx);
7207 assert_eq!(editor.text(cx), "bac");
7208 assert_eq!(
7209 editor.selections.ranges(&editor.display_snapshot(cx)),
7210 [MultiBufferOffset(3)..MultiBufferOffset(3)]
7211 );
7212
7213 editor
7214 });
7215
7216 _ = cx.add_window(|window, cx| {
7217 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
7218 editor.set_style(EditorStyle::default(), window, cx);
7219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7220 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
7221 });
7222 editor.transpose(&Default::default(), window, cx);
7223 assert_eq!(editor.text(cx), "acb\nde");
7224 assert_eq!(
7225 editor.selections.ranges(&editor.display_snapshot(cx)),
7226 [MultiBufferOffset(3)..MultiBufferOffset(3)]
7227 );
7228
7229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7230 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
7231 });
7232 editor.transpose(&Default::default(), window, cx);
7233 assert_eq!(editor.text(cx), "acbd\ne");
7234 assert_eq!(
7235 editor.selections.ranges(&editor.display_snapshot(cx)),
7236 [MultiBufferOffset(5)..MultiBufferOffset(5)]
7237 );
7238
7239 editor.transpose(&Default::default(), window, cx);
7240 assert_eq!(editor.text(cx), "acbde\n");
7241 assert_eq!(
7242 editor.selections.ranges(&editor.display_snapshot(cx)),
7243 [MultiBufferOffset(6)..MultiBufferOffset(6)]
7244 );
7245
7246 editor.transpose(&Default::default(), window, cx);
7247 assert_eq!(editor.text(cx), "acbd\ne");
7248 assert_eq!(
7249 editor.selections.ranges(&editor.display_snapshot(cx)),
7250 [MultiBufferOffset(6)..MultiBufferOffset(6)]
7251 );
7252
7253 editor
7254 });
7255
7256 _ = cx.add_window(|window, cx| {
7257 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
7258 editor.set_style(EditorStyle::default(), window, cx);
7259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7260 s.select_ranges([
7261 MultiBufferOffset(1)..MultiBufferOffset(1),
7262 MultiBufferOffset(2)..MultiBufferOffset(2),
7263 MultiBufferOffset(4)..MultiBufferOffset(4),
7264 ])
7265 });
7266 editor.transpose(&Default::default(), window, cx);
7267 assert_eq!(editor.text(cx), "bacd\ne");
7268 assert_eq!(
7269 editor.selections.ranges(&editor.display_snapshot(cx)),
7270 [
7271 MultiBufferOffset(2)..MultiBufferOffset(2),
7272 MultiBufferOffset(3)..MultiBufferOffset(3),
7273 MultiBufferOffset(5)..MultiBufferOffset(5)
7274 ]
7275 );
7276
7277 editor.transpose(&Default::default(), window, cx);
7278 assert_eq!(editor.text(cx), "bcade\n");
7279 assert_eq!(
7280 editor.selections.ranges(&editor.display_snapshot(cx)),
7281 [
7282 MultiBufferOffset(3)..MultiBufferOffset(3),
7283 MultiBufferOffset(4)..MultiBufferOffset(4),
7284 MultiBufferOffset(6)..MultiBufferOffset(6)
7285 ]
7286 );
7287
7288 editor.transpose(&Default::default(), window, cx);
7289 assert_eq!(editor.text(cx), "bcda\ne");
7290 assert_eq!(
7291 editor.selections.ranges(&editor.display_snapshot(cx)),
7292 [
7293 MultiBufferOffset(4)..MultiBufferOffset(4),
7294 MultiBufferOffset(6)..MultiBufferOffset(6)
7295 ]
7296 );
7297
7298 editor.transpose(&Default::default(), window, cx);
7299 assert_eq!(editor.text(cx), "bcade\n");
7300 assert_eq!(
7301 editor.selections.ranges(&editor.display_snapshot(cx)),
7302 [
7303 MultiBufferOffset(4)..MultiBufferOffset(4),
7304 MultiBufferOffset(6)..MultiBufferOffset(6)
7305 ]
7306 );
7307
7308 editor.transpose(&Default::default(), window, cx);
7309 assert_eq!(editor.text(cx), "bcaed\n");
7310 assert_eq!(
7311 editor.selections.ranges(&editor.display_snapshot(cx)),
7312 [
7313 MultiBufferOffset(5)..MultiBufferOffset(5),
7314 MultiBufferOffset(6)..MultiBufferOffset(6)
7315 ]
7316 );
7317
7318 editor
7319 });
7320
7321 _ = cx.add_window(|window, cx| {
7322 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
7323 editor.set_style(EditorStyle::default(), window, cx);
7324 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7325 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
7326 });
7327 editor.transpose(&Default::default(), window, cx);
7328 assert_eq!(editor.text(cx), "🏀🍐✋");
7329 assert_eq!(
7330 editor.selections.ranges(&editor.display_snapshot(cx)),
7331 [MultiBufferOffset(8)..MultiBufferOffset(8)]
7332 );
7333
7334 editor.transpose(&Default::default(), window, cx);
7335 assert_eq!(editor.text(cx), "🏀✋🍐");
7336 assert_eq!(
7337 editor.selections.ranges(&editor.display_snapshot(cx)),
7338 [MultiBufferOffset(11)..MultiBufferOffset(11)]
7339 );
7340
7341 editor.transpose(&Default::default(), window, cx);
7342 assert_eq!(editor.text(cx), "🏀🍐✋");
7343 assert_eq!(
7344 editor.selections.ranges(&editor.display_snapshot(cx)),
7345 [MultiBufferOffset(11)..MultiBufferOffset(11)]
7346 );
7347
7348 editor
7349 });
7350}
7351
7352#[gpui::test]
7353async fn test_rewrap(cx: &mut TestAppContext) {
7354 init_test(cx, |settings| {
7355 settings.languages.0.extend([
7356 (
7357 "Markdown".into(),
7358 LanguageSettingsContent {
7359 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
7360 preferred_line_length: Some(40),
7361 ..Default::default()
7362 },
7363 ),
7364 (
7365 "Plain Text".into(),
7366 LanguageSettingsContent {
7367 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
7368 preferred_line_length: Some(40),
7369 ..Default::default()
7370 },
7371 ),
7372 (
7373 "C++".into(),
7374 LanguageSettingsContent {
7375 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7376 preferred_line_length: Some(40),
7377 ..Default::default()
7378 },
7379 ),
7380 (
7381 "Python".into(),
7382 LanguageSettingsContent {
7383 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7384 preferred_line_length: Some(40),
7385 ..Default::default()
7386 },
7387 ),
7388 (
7389 "Rust".into(),
7390 LanguageSettingsContent {
7391 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7392 preferred_line_length: Some(40),
7393 ..Default::default()
7394 },
7395 ),
7396 ])
7397 });
7398
7399 let mut cx = EditorTestContext::new(cx).await;
7400
7401 let cpp_language = Arc::new(Language::new(
7402 LanguageConfig {
7403 name: "C++".into(),
7404 line_comments: vec!["// ".into()],
7405 ..LanguageConfig::default()
7406 },
7407 None,
7408 ));
7409 let python_language = Arc::new(Language::new(
7410 LanguageConfig {
7411 name: "Python".into(),
7412 line_comments: vec!["# ".into()],
7413 ..LanguageConfig::default()
7414 },
7415 None,
7416 ));
7417 let markdown_language = Arc::new(Language::new(
7418 LanguageConfig {
7419 name: "Markdown".into(),
7420 rewrap_prefixes: vec![
7421 regex::Regex::new("\\d+\\.\\s+").unwrap(),
7422 regex::Regex::new("[-*+]\\s+").unwrap(),
7423 ],
7424 ..LanguageConfig::default()
7425 },
7426 None,
7427 ));
7428 let rust_language = Arc::new(
7429 Language::new(
7430 LanguageConfig {
7431 name: "Rust".into(),
7432 line_comments: vec!["// ".into(), "/// ".into()],
7433 ..LanguageConfig::default()
7434 },
7435 Some(tree_sitter_rust::LANGUAGE.into()),
7436 )
7437 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
7438 .unwrap(),
7439 );
7440
7441 let plaintext_language = Arc::new(Language::new(
7442 LanguageConfig {
7443 name: "Plain Text".into(),
7444 ..LanguageConfig::default()
7445 },
7446 None,
7447 ));
7448
7449 // Test basic rewrapping of a long line with a cursor
7450 assert_rewrap(
7451 indoc! {"
7452 // ˇThis is a long comment that needs to be wrapped.
7453 "},
7454 indoc! {"
7455 // ˇThis is a long comment that needs to
7456 // be wrapped.
7457 "},
7458 cpp_language.clone(),
7459 &mut cx,
7460 );
7461
7462 // Test rewrapping a full selection
7463 assert_rewrap(
7464 indoc! {"
7465 «// This selected long comment needs to be wrapped.ˇ»"
7466 },
7467 indoc! {"
7468 «// This selected long comment needs to
7469 // be wrapped.ˇ»"
7470 },
7471 cpp_language.clone(),
7472 &mut cx,
7473 );
7474
7475 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
7476 assert_rewrap(
7477 indoc! {"
7478 // ˇThis is the first line.
7479 // Thisˇ is the second line.
7480 // This is the thirdˇ line, all part of one paragraph.
7481 "},
7482 indoc! {"
7483 // ˇThis is the first line. Thisˇ is the
7484 // second line. This is the thirdˇ line,
7485 // all part of one paragraph.
7486 "},
7487 cpp_language.clone(),
7488 &mut cx,
7489 );
7490
7491 // Test multiple cursors in different paragraphs trigger separate rewraps
7492 assert_rewrap(
7493 indoc! {"
7494 // ˇThis is the first paragraph, first line.
7495 // ˇThis is the first paragraph, second line.
7496
7497 // ˇThis is the second paragraph, first line.
7498 // ˇThis is the second paragraph, second line.
7499 "},
7500 indoc! {"
7501 // ˇThis is the first paragraph, first
7502 // line. ˇThis is the first paragraph,
7503 // second line.
7504
7505 // ˇThis is the second paragraph, first
7506 // line. ˇThis is the second paragraph,
7507 // second line.
7508 "},
7509 cpp_language.clone(),
7510 &mut cx,
7511 );
7512
7513 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
7514 assert_rewrap(
7515 indoc! {"
7516 «// A regular long long comment to be wrapped.
7517 /// A documentation long comment to be wrapped.ˇ»
7518 "},
7519 indoc! {"
7520 «// A regular long long comment to be
7521 // wrapped.
7522 /// A documentation long comment to be
7523 /// wrapped.ˇ»
7524 "},
7525 rust_language.clone(),
7526 &mut cx,
7527 );
7528
7529 // Test that change in indentation level trigger seperate rewraps
7530 assert_rewrap(
7531 indoc! {"
7532 fn foo() {
7533 «// This is a long comment at the base indent.
7534 // This is a long comment at the next indent.ˇ»
7535 }
7536 "},
7537 indoc! {"
7538 fn foo() {
7539 «// This is a long comment at the
7540 // base indent.
7541 // This is a long comment at the
7542 // next indent.ˇ»
7543 }
7544 "},
7545 rust_language.clone(),
7546 &mut cx,
7547 );
7548
7549 // Test that different comment prefix characters (e.g., '#') are handled correctly
7550 assert_rewrap(
7551 indoc! {"
7552 # ˇThis is a long comment using a pound sign.
7553 "},
7554 indoc! {"
7555 # ˇThis is a long comment using a pound
7556 # sign.
7557 "},
7558 python_language,
7559 &mut cx,
7560 );
7561
7562 // Test rewrapping only affects comments, not code even when selected
7563 assert_rewrap(
7564 indoc! {"
7565 «/// This doc comment is long and should be wrapped.
7566 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
7567 "},
7568 indoc! {"
7569 «/// This doc comment is long and should
7570 /// be wrapped.
7571 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
7572 "},
7573 rust_language.clone(),
7574 &mut cx,
7575 );
7576
7577 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
7578 assert_rewrap(
7579 indoc! {"
7580 # Header
7581
7582 A long long long line of markdown text to wrap.ˇ
7583 "},
7584 indoc! {"
7585 # Header
7586
7587 A long long long line of markdown text
7588 to wrap.ˇ
7589 "},
7590 markdown_language.clone(),
7591 &mut cx,
7592 );
7593
7594 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
7595 assert_rewrap(
7596 indoc! {"
7597 «1. This is a numbered list item that is very long and needs to be wrapped properly.
7598 2. This is a numbered list item that is very long and needs to be wrapped properly.
7599 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
7600 "},
7601 indoc! {"
7602 «1. This is a numbered list item that is
7603 very long and needs to be wrapped
7604 properly.
7605 2. This is a numbered list item that is
7606 very long and needs to be wrapped
7607 properly.
7608 - This is an unordered list item that is
7609 also very long and should not merge
7610 with the numbered item.ˇ»
7611 "},
7612 markdown_language.clone(),
7613 &mut cx,
7614 );
7615
7616 // Test that rewrapping add indents for rewrapping boundary if not exists already.
7617 assert_rewrap(
7618 indoc! {"
7619 «1. This is a numbered list item that is
7620 very long and needs to be wrapped
7621 properly.
7622 2. This is a numbered list item that is
7623 very long and needs to be wrapped
7624 properly.
7625 - This is an unordered list item that is
7626 also very long and should not merge with
7627 the numbered item.ˇ»
7628 "},
7629 indoc! {"
7630 «1. This is a numbered list item that is
7631 very long and needs to be wrapped
7632 properly.
7633 2. This is a numbered list item that is
7634 very long and needs to be wrapped
7635 properly.
7636 - This is an unordered list item that is
7637 also very long and should not merge
7638 with the numbered item.ˇ»
7639 "},
7640 markdown_language.clone(),
7641 &mut cx,
7642 );
7643
7644 // Test that rewrapping maintain indents even when they already exists.
7645 assert_rewrap(
7646 indoc! {"
7647 «1. This is a numbered list
7648 item that is very long and needs to be wrapped properly.
7649 2. This is a numbered list
7650 item that is very long and needs to be wrapped properly.
7651 - This is an unordered list item that is also very long and
7652 should not merge with the numbered item.ˇ»
7653 "},
7654 indoc! {"
7655 «1. This is a numbered list item that is
7656 very long and needs to be wrapped
7657 properly.
7658 2. This is a numbered list item that is
7659 very long and needs to be wrapped
7660 properly.
7661 - This is an unordered list item that is
7662 also very long and should not merge
7663 with the numbered item.ˇ»
7664 "},
7665 markdown_language.clone(),
7666 &mut cx,
7667 );
7668
7669 // Test that empty selection rewrap on a numbered list item does not merge adjacent items
7670 assert_rewrap(
7671 indoc! {"
7672 1. This is the first numbered list item that is very long and needs to be wrapped properly.
7673 2. ˇThis is the second numbered list item that is also very long and needs to be wrapped.
7674 3. This is the third numbered list item, shorter.
7675 "},
7676 indoc! {"
7677 1. This is the first numbered list item
7678 that is very long and needs to be
7679 wrapped properly.
7680 2. ˇThis is the second numbered list item
7681 that is also very long and needs to
7682 be wrapped.
7683 3. This is the third numbered list item,
7684 shorter.
7685 "},
7686 markdown_language.clone(),
7687 &mut cx,
7688 );
7689
7690 // Test that empty selection rewrap on a bullet list item does not merge adjacent items
7691 assert_rewrap(
7692 indoc! {"
7693 - This is the first bullet item that is very long and needs wrapping properly here.
7694 - ˇThis is the second bullet item that is also very long and needs to be wrapped.
7695 - This is the third bullet item, shorter.
7696 "},
7697 indoc! {"
7698 - This is the first bullet item that is
7699 very long and needs wrapping properly
7700 here.
7701 - ˇThis is the second bullet item that is
7702 also very long and needs to be
7703 wrapped.
7704 - This is the third bullet item,
7705 shorter.
7706 "},
7707 markdown_language,
7708 &mut cx,
7709 );
7710
7711 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
7712 assert_rewrap(
7713 indoc! {"
7714 ˇThis is a very long line of plain text that will be wrapped.
7715 "},
7716 indoc! {"
7717 ˇThis is a very long line of plain text
7718 that will be wrapped.
7719 "},
7720 plaintext_language.clone(),
7721 &mut cx,
7722 );
7723
7724 // Test that non-commented code acts as a paragraph boundary within a selection
7725 assert_rewrap(
7726 indoc! {"
7727 «// This is the first long comment block to be wrapped.
7728 fn my_func(a: u32);
7729 // This is the second long comment block to be wrapped.ˇ»
7730 "},
7731 indoc! {"
7732 «// This is the first long comment block
7733 // to be wrapped.
7734 fn my_func(a: u32);
7735 // This is the second long comment block
7736 // to be wrapped.ˇ»
7737 "},
7738 rust_language,
7739 &mut cx,
7740 );
7741
7742 // Test rewrapping multiple selections, including ones with blank lines or tabs
7743 assert_rewrap(
7744 indoc! {"
7745 «ˇThis is a very long line that will be wrapped.
7746
7747 This is another paragraph in the same selection.»
7748
7749 «\tThis is a very long indented line that will be wrapped.ˇ»
7750 "},
7751 indoc! {"
7752 «ˇThis is a very long line that will be
7753 wrapped.
7754
7755 This is another paragraph in the same
7756 selection.»
7757
7758 «\tThis is a very long indented line
7759 \tthat will be wrapped.ˇ»
7760 "},
7761 plaintext_language,
7762 &mut cx,
7763 );
7764
7765 // Test that an empty comment line acts as a paragraph boundary
7766 assert_rewrap(
7767 indoc! {"
7768 // ˇThis is a long comment that will be wrapped.
7769 //
7770 // And this is another long comment that will also be wrapped.ˇ
7771 "},
7772 indoc! {"
7773 // ˇThis is a long comment that will be
7774 // wrapped.
7775 //
7776 // And this is another long comment that
7777 // will also be wrapped.ˇ
7778 "},
7779 cpp_language,
7780 &mut cx,
7781 );
7782
7783 #[track_caller]
7784 fn assert_rewrap(
7785 unwrapped_text: &str,
7786 wrapped_text: &str,
7787 language: Arc<Language>,
7788 cx: &mut EditorTestContext,
7789 ) {
7790 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7791 cx.set_state(unwrapped_text);
7792 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7793 cx.assert_editor_state(wrapped_text);
7794 }
7795}
7796
7797#[gpui::test]
7798async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
7799 init_test(cx, |settings| {
7800 settings.languages.0.extend([(
7801 "Rust".into(),
7802 LanguageSettingsContent {
7803 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7804 preferred_line_length: Some(40),
7805 ..Default::default()
7806 },
7807 )])
7808 });
7809
7810 let mut cx = EditorTestContext::new(cx).await;
7811
7812 let rust_lang = Arc::new(
7813 Language::new(
7814 LanguageConfig {
7815 name: "Rust".into(),
7816 line_comments: vec!["// ".into()],
7817 block_comment: Some(BlockCommentConfig {
7818 start: "/*".into(),
7819 end: "*/".into(),
7820 prefix: "* ".into(),
7821 tab_size: 1,
7822 }),
7823 documentation_comment: Some(BlockCommentConfig {
7824 start: "/**".into(),
7825 end: "*/".into(),
7826 prefix: "* ".into(),
7827 tab_size: 1,
7828 }),
7829
7830 ..LanguageConfig::default()
7831 },
7832 Some(tree_sitter_rust::LANGUAGE.into()),
7833 )
7834 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
7835 .unwrap(),
7836 );
7837
7838 // regular block comment
7839 assert_rewrap(
7840 indoc! {"
7841 /*
7842 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7843 */
7844 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7845 "},
7846 indoc! {"
7847 /*
7848 *ˇ Lorem ipsum dolor sit amet,
7849 * consectetur adipiscing elit.
7850 */
7851 /*
7852 *ˇ Lorem ipsum dolor sit amet,
7853 * consectetur adipiscing elit.
7854 */
7855 "},
7856 rust_lang.clone(),
7857 &mut cx,
7858 );
7859
7860 // indent is respected
7861 assert_rewrap(
7862 indoc! {"
7863 {}
7864 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7865 "},
7866 indoc! {"
7867 {}
7868 /*
7869 *ˇ Lorem ipsum dolor sit amet,
7870 * consectetur adipiscing elit.
7871 */
7872 "},
7873 rust_lang.clone(),
7874 &mut cx,
7875 );
7876
7877 // short block comments with inline delimiters
7878 assert_rewrap(
7879 indoc! {"
7880 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7881 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7882 */
7883 /*
7884 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7885 "},
7886 indoc! {"
7887 /*
7888 *ˇ Lorem ipsum dolor sit amet,
7889 * consectetur adipiscing elit.
7890 */
7891 /*
7892 *ˇ Lorem ipsum dolor sit amet,
7893 * consectetur adipiscing elit.
7894 */
7895 /*
7896 *ˇ Lorem ipsum dolor sit amet,
7897 * consectetur adipiscing elit.
7898 */
7899 "},
7900 rust_lang.clone(),
7901 &mut cx,
7902 );
7903
7904 // multiline block comment with inline start/end delimiters
7905 assert_rewrap(
7906 indoc! {"
7907 /*ˇ Lorem ipsum dolor sit amet,
7908 * consectetur adipiscing elit. */
7909 "},
7910 indoc! {"
7911 /*
7912 *ˇ Lorem ipsum dolor sit amet,
7913 * consectetur adipiscing elit.
7914 */
7915 "},
7916 rust_lang.clone(),
7917 &mut cx,
7918 );
7919
7920 // block comment rewrap still respects paragraph bounds
7921 assert_rewrap(
7922 indoc! {"
7923 /*
7924 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7925 *
7926 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7927 */
7928 "},
7929 indoc! {"
7930 /*
7931 *ˇ Lorem ipsum dolor sit amet,
7932 * consectetur adipiscing elit.
7933 *
7934 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7935 */
7936 "},
7937 rust_lang.clone(),
7938 &mut cx,
7939 );
7940
7941 // documentation comments
7942 assert_rewrap(
7943 indoc! {"
7944 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7945 /**
7946 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7947 */
7948 "},
7949 indoc! {"
7950 /**
7951 *ˇ Lorem ipsum dolor sit amet,
7952 * consectetur adipiscing elit.
7953 */
7954 /**
7955 *ˇ Lorem ipsum dolor sit amet,
7956 * consectetur adipiscing elit.
7957 */
7958 "},
7959 rust_lang.clone(),
7960 &mut cx,
7961 );
7962
7963 // different, adjacent comments
7964 assert_rewrap(
7965 indoc! {"
7966 /**
7967 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7968 */
7969 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7970 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7971 "},
7972 indoc! {"
7973 /**
7974 *ˇ Lorem ipsum dolor sit amet,
7975 * consectetur adipiscing elit.
7976 */
7977 /*
7978 *ˇ Lorem ipsum dolor sit amet,
7979 * consectetur adipiscing elit.
7980 */
7981 //ˇ Lorem ipsum dolor sit amet,
7982 // consectetur adipiscing elit.
7983 "},
7984 rust_lang.clone(),
7985 &mut cx,
7986 );
7987
7988 // selection w/ single short block comment
7989 assert_rewrap(
7990 indoc! {"
7991 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7992 "},
7993 indoc! {"
7994 «/*
7995 * Lorem ipsum dolor sit amet,
7996 * consectetur adipiscing elit.
7997 */ˇ»
7998 "},
7999 rust_lang.clone(),
8000 &mut cx,
8001 );
8002
8003 // rewrapping a single comment w/ abutting comments
8004 assert_rewrap(
8005 indoc! {"
8006 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
8007 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
8008 "},
8009 indoc! {"
8010 /*
8011 * ˇLorem ipsum dolor sit amet,
8012 * consectetur adipiscing elit.
8013 */
8014 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
8015 "},
8016 rust_lang.clone(),
8017 &mut cx,
8018 );
8019
8020 // selection w/ non-abutting short block comments
8021 assert_rewrap(
8022 indoc! {"
8023 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
8024
8025 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
8026 "},
8027 indoc! {"
8028 «/*
8029 * Lorem ipsum dolor sit amet,
8030 * consectetur adipiscing elit.
8031 */
8032
8033 /*
8034 * Lorem ipsum dolor sit amet,
8035 * consectetur adipiscing elit.
8036 */ˇ»
8037 "},
8038 rust_lang.clone(),
8039 &mut cx,
8040 );
8041
8042 // selection of multiline block comments
8043 assert_rewrap(
8044 indoc! {"
8045 «/* Lorem ipsum dolor sit amet,
8046 * consectetur adipiscing elit. */ˇ»
8047 "},
8048 indoc! {"
8049 «/*
8050 * Lorem ipsum dolor sit amet,
8051 * consectetur adipiscing elit.
8052 */ˇ»
8053 "},
8054 rust_lang.clone(),
8055 &mut cx,
8056 );
8057
8058 // partial selection of multiline block comments
8059 assert_rewrap(
8060 indoc! {"
8061 «/* Lorem ipsum dolor sit amet,ˇ»
8062 * consectetur adipiscing elit. */
8063 /* Lorem ipsum dolor sit amet,
8064 «* consectetur adipiscing elit. */ˇ»
8065 "},
8066 indoc! {"
8067 «/*
8068 * Lorem ipsum dolor sit amet,ˇ»
8069 * consectetur adipiscing elit. */
8070 /* Lorem ipsum dolor sit amet,
8071 «* consectetur adipiscing elit.
8072 */ˇ»
8073 "},
8074 rust_lang.clone(),
8075 &mut cx,
8076 );
8077
8078 // selection w/ abutting short block comments
8079 // TODO: should not be combined; should rewrap as 2 comments
8080 assert_rewrap(
8081 indoc! {"
8082 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
8083 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
8084 "},
8085 // desired behavior:
8086 // indoc! {"
8087 // «/*
8088 // * Lorem ipsum dolor sit amet,
8089 // * consectetur adipiscing elit.
8090 // */
8091 // /*
8092 // * Lorem ipsum dolor sit amet,
8093 // * consectetur adipiscing elit.
8094 // */ˇ»
8095 // "},
8096 // actual behaviour:
8097 indoc! {"
8098 «/*
8099 * Lorem ipsum dolor sit amet,
8100 * consectetur adipiscing elit. Lorem
8101 * ipsum dolor sit amet, consectetur
8102 * adipiscing elit.
8103 */ˇ»
8104 "},
8105 rust_lang.clone(),
8106 &mut cx,
8107 );
8108
8109 // TODO: same as above, but with delimiters on separate line
8110 // assert_rewrap(
8111 // indoc! {"
8112 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
8113 // */
8114 // /*
8115 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
8116 // "},
8117 // // desired:
8118 // // indoc! {"
8119 // // «/*
8120 // // * Lorem ipsum dolor sit amet,
8121 // // * consectetur adipiscing elit.
8122 // // */
8123 // // /*
8124 // // * Lorem ipsum dolor sit amet,
8125 // // * consectetur adipiscing elit.
8126 // // */ˇ»
8127 // // "},
8128 // // actual: (but with trailing w/s on the empty lines)
8129 // indoc! {"
8130 // «/*
8131 // * Lorem ipsum dolor sit amet,
8132 // * consectetur adipiscing elit.
8133 // *
8134 // */
8135 // /*
8136 // *
8137 // * Lorem ipsum dolor sit amet,
8138 // * consectetur adipiscing elit.
8139 // */ˇ»
8140 // "},
8141 // rust_lang.clone(),
8142 // &mut cx,
8143 // );
8144
8145 // TODO these are unhandled edge cases; not correct, just documenting known issues
8146 assert_rewrap(
8147 indoc! {"
8148 /*
8149 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
8150 */
8151 /*
8152 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
8153 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
8154 "},
8155 // desired:
8156 // indoc! {"
8157 // /*
8158 // *ˇ Lorem ipsum dolor sit amet,
8159 // * consectetur adipiscing elit.
8160 // */
8161 // /*
8162 // *ˇ Lorem ipsum dolor sit amet,
8163 // * consectetur adipiscing elit.
8164 // */
8165 // /*
8166 // *ˇ Lorem ipsum dolor sit amet
8167 // */ /* consectetur adipiscing elit. */
8168 // "},
8169 // actual:
8170 indoc! {"
8171 /*
8172 //ˇ Lorem ipsum dolor sit amet,
8173 // consectetur adipiscing elit.
8174 */
8175 /*
8176 * //ˇ Lorem ipsum dolor sit amet,
8177 * consectetur adipiscing elit.
8178 */
8179 /*
8180 *ˇ Lorem ipsum dolor sit amet */ /*
8181 * consectetur adipiscing elit.
8182 */
8183 "},
8184 rust_lang,
8185 &mut cx,
8186 );
8187
8188 #[track_caller]
8189 fn assert_rewrap(
8190 unwrapped_text: &str,
8191 wrapped_text: &str,
8192 language: Arc<Language>,
8193 cx: &mut EditorTestContext,
8194 ) {
8195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
8196 cx.set_state(unwrapped_text);
8197 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
8198 cx.assert_editor_state(wrapped_text);
8199 }
8200}
8201
8202#[gpui::test]
8203async fn test_hard_wrap(cx: &mut TestAppContext) {
8204 init_test(cx, |_| {});
8205 let mut cx = EditorTestContext::new(cx).await;
8206
8207 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
8208 cx.update_editor(|editor, _, cx| {
8209 editor.set_hard_wrap(Some(14), cx);
8210 });
8211
8212 cx.set_state(indoc!(
8213 "
8214 one two three ˇ
8215 "
8216 ));
8217 cx.simulate_input("four");
8218 cx.run_until_parked();
8219
8220 cx.assert_editor_state(indoc!(
8221 "
8222 one two three
8223 fourˇ
8224 "
8225 ));
8226
8227 cx.update_editor(|editor, window, cx| {
8228 editor.newline(&Default::default(), window, cx);
8229 });
8230 cx.run_until_parked();
8231 cx.assert_editor_state(indoc!(
8232 "
8233 one two three
8234 four
8235 ˇ
8236 "
8237 ));
8238
8239 cx.simulate_input("five");
8240 cx.run_until_parked();
8241 cx.assert_editor_state(indoc!(
8242 "
8243 one two three
8244 four
8245 fiveˇ
8246 "
8247 ));
8248
8249 cx.update_editor(|editor, window, cx| {
8250 editor.newline(&Default::default(), window, cx);
8251 });
8252 cx.run_until_parked();
8253 cx.simulate_input("# ");
8254 cx.run_until_parked();
8255 cx.assert_editor_state(indoc!(
8256 "
8257 one two three
8258 four
8259 five
8260 # ˇ
8261 "
8262 ));
8263
8264 cx.update_editor(|editor, window, cx| {
8265 editor.newline(&Default::default(), window, cx);
8266 });
8267 cx.run_until_parked();
8268 cx.assert_editor_state(indoc!(
8269 "
8270 one two three
8271 four
8272 five
8273 #\x20
8274 #ˇ
8275 "
8276 ));
8277
8278 cx.simulate_input(" 6");
8279 cx.run_until_parked();
8280 cx.assert_editor_state(indoc!(
8281 "
8282 one two three
8283 four
8284 five
8285 #
8286 # 6ˇ
8287 "
8288 ));
8289}
8290
8291#[gpui::test]
8292async fn test_cut_line_ends(cx: &mut TestAppContext) {
8293 init_test(cx, |_| {});
8294
8295 let mut cx = EditorTestContext::new(cx).await;
8296
8297 cx.set_state(indoc! {"The quick brownˇ"});
8298 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
8299 cx.assert_editor_state(indoc! {"The quick brownˇ"});
8300
8301 cx.set_state(indoc! {"The emacs foxˇ"});
8302 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
8303 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
8304
8305 cx.set_state(indoc! {"
8306 The quick« brownˇ»
8307 fox jumps overˇ
8308 the lazy dog"});
8309 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8310 cx.assert_editor_state(indoc! {"
8311 The quickˇ
8312 ˇthe lazy dog"});
8313
8314 cx.set_state(indoc! {"
8315 The quick« brownˇ»
8316 fox jumps overˇ
8317 the lazy dog"});
8318 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
8319 cx.assert_editor_state(indoc! {"
8320 The quickˇ
8321 fox jumps overˇthe lazy dog"});
8322
8323 cx.set_state(indoc! {"
8324 The quick« brownˇ»
8325 fox jumps overˇ
8326 the lazy dog"});
8327 cx.update_editor(|e, window, cx| {
8328 e.cut_to_end_of_line(
8329 &CutToEndOfLine {
8330 stop_at_newlines: true,
8331 },
8332 window,
8333 cx,
8334 )
8335 });
8336 cx.assert_editor_state(indoc! {"
8337 The quickˇ
8338 fox jumps overˇ
8339 the lazy dog"});
8340
8341 cx.set_state(indoc! {"
8342 The quick« brownˇ»
8343 fox jumps overˇ
8344 the lazy dog"});
8345 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
8346 cx.assert_editor_state(indoc! {"
8347 The quickˇ
8348 fox jumps overˇthe lazy dog"});
8349}
8350
8351#[gpui::test]
8352async fn test_clipboard(cx: &mut TestAppContext) {
8353 init_test(cx, |_| {});
8354
8355 let mut cx = EditorTestContext::new(cx).await;
8356
8357 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
8358 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8359 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
8360
8361 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
8362 cx.set_state("two ˇfour ˇsix ˇ");
8363 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8364 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
8365
8366 // Paste again but with only two cursors. Since the number of cursors doesn't
8367 // match the number of slices in the clipboard, the entire clipboard text
8368 // is pasted at each cursor.
8369 cx.set_state("ˇtwo one✅ four three six five ˇ");
8370 cx.update_editor(|e, window, cx| {
8371 e.handle_input("( ", window, cx);
8372 e.paste(&Paste, window, cx);
8373 e.handle_input(") ", window, cx);
8374 });
8375 cx.assert_editor_state(
8376 &([
8377 "( one✅ ",
8378 "three ",
8379 "five ) ˇtwo one✅ four three six five ( one✅ ",
8380 "three ",
8381 "five ) ˇ",
8382 ]
8383 .join("\n")),
8384 );
8385
8386 // Cut with three selections, one of which is full-line.
8387 cx.set_state(indoc! {"
8388 1«2ˇ»3
8389 4ˇ567
8390 «8ˇ»9"});
8391 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8392 cx.assert_editor_state(indoc! {"
8393 1ˇ3
8394 ˇ9"});
8395
8396 // Paste with three selections, noticing how the copied selection that was full-line
8397 // gets inserted before the second cursor.
8398 cx.set_state(indoc! {"
8399 1ˇ3
8400 9ˇ
8401 «oˇ»ne"});
8402 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8403 cx.assert_editor_state(indoc! {"
8404 12ˇ3
8405 4567
8406 9ˇ
8407 8ˇne"});
8408
8409 // Copy with a single cursor only, which writes the whole line into the clipboard.
8410 cx.set_state(indoc! {"
8411 The quick brown
8412 fox juˇmps over
8413 the lazy dog"});
8414 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8415 assert_eq!(
8416 cx.read_from_clipboard()
8417 .and_then(|item| item.text().as_deref().map(str::to_string)),
8418 Some("fox jumps over\n".to_string())
8419 );
8420
8421 // Paste with three selections, noticing how the copied full-line selection is inserted
8422 // before the empty selections but replaces the selection that is non-empty.
8423 cx.set_state(indoc! {"
8424 Tˇhe quick brown
8425 «foˇ»x jumps over
8426 tˇhe lazy dog"});
8427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8428 cx.assert_editor_state(indoc! {"
8429 fox jumps over
8430 Tˇhe quick brown
8431 fox jumps over
8432 ˇx jumps over
8433 fox jumps over
8434 tˇhe lazy dog"});
8435}
8436
8437#[gpui::test]
8438async fn test_copy_trim(cx: &mut TestAppContext) {
8439 init_test(cx, |_| {});
8440
8441 let mut cx = EditorTestContext::new(cx).await;
8442 cx.set_state(
8443 r#" «for selection in selections.iter() {
8444 let mut start = selection.start;
8445 let mut end = selection.end;
8446 let is_entire_line = selection.is_empty();
8447 if is_entire_line {
8448 start = Point::new(start.row, 0);ˇ»
8449 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8450 }
8451 "#,
8452 );
8453 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8454 assert_eq!(
8455 cx.read_from_clipboard()
8456 .and_then(|item| item.text().as_deref().map(str::to_string)),
8457 Some(
8458 "for selection in selections.iter() {
8459 let mut start = selection.start;
8460 let mut end = selection.end;
8461 let is_entire_line = selection.is_empty();
8462 if is_entire_line {
8463 start = Point::new(start.row, 0);"
8464 .to_string()
8465 ),
8466 "Regular copying preserves all indentation selected",
8467 );
8468 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8469 assert_eq!(
8470 cx.read_from_clipboard()
8471 .and_then(|item| item.text().as_deref().map(str::to_string)),
8472 Some(
8473 "for selection in selections.iter() {
8474let mut start = selection.start;
8475let mut end = selection.end;
8476let is_entire_line = selection.is_empty();
8477if is_entire_line {
8478 start = Point::new(start.row, 0);"
8479 .to_string()
8480 ),
8481 "Copying with stripping should strip all leading whitespaces"
8482 );
8483
8484 cx.set_state(
8485 r#" « for selection in selections.iter() {
8486 let mut start = selection.start;
8487 let mut end = selection.end;
8488 let is_entire_line = selection.is_empty();
8489 if is_entire_line {
8490 start = Point::new(start.row, 0);ˇ»
8491 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8492 }
8493 "#,
8494 );
8495 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8496 assert_eq!(
8497 cx.read_from_clipboard()
8498 .and_then(|item| item.text().as_deref().map(str::to_string)),
8499 Some(
8500 " for selection in selections.iter() {
8501 let mut start = selection.start;
8502 let mut end = selection.end;
8503 let is_entire_line = selection.is_empty();
8504 if is_entire_line {
8505 start = Point::new(start.row, 0);"
8506 .to_string()
8507 ),
8508 "Regular copying preserves all indentation selected",
8509 );
8510 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8511 assert_eq!(
8512 cx.read_from_clipboard()
8513 .and_then(|item| item.text().as_deref().map(str::to_string)),
8514 Some(
8515 "for selection in selections.iter() {
8516let mut start = selection.start;
8517let mut end = selection.end;
8518let is_entire_line = selection.is_empty();
8519if is_entire_line {
8520 start = Point::new(start.row, 0);"
8521 .to_string()
8522 ),
8523 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
8524 );
8525
8526 cx.set_state(
8527 r#" «ˇ for selection in selections.iter() {
8528 let mut start = selection.start;
8529 let mut end = selection.end;
8530 let is_entire_line = selection.is_empty();
8531 if is_entire_line {
8532 start = Point::new(start.row, 0);»
8533 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8534 }
8535 "#,
8536 );
8537 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8538 assert_eq!(
8539 cx.read_from_clipboard()
8540 .and_then(|item| item.text().as_deref().map(str::to_string)),
8541 Some(
8542 " for selection in selections.iter() {
8543 let mut start = selection.start;
8544 let mut end = selection.end;
8545 let is_entire_line = selection.is_empty();
8546 if is_entire_line {
8547 start = Point::new(start.row, 0);"
8548 .to_string()
8549 ),
8550 "Regular copying for reverse selection works the same",
8551 );
8552 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8553 assert_eq!(
8554 cx.read_from_clipboard()
8555 .and_then(|item| item.text().as_deref().map(str::to_string)),
8556 Some(
8557 "for selection in selections.iter() {
8558let mut start = selection.start;
8559let mut end = selection.end;
8560let is_entire_line = selection.is_empty();
8561if is_entire_line {
8562 start = Point::new(start.row, 0);"
8563 .to_string()
8564 ),
8565 "Copying with stripping for reverse selection works the same"
8566 );
8567
8568 cx.set_state(
8569 r#" for selection «in selections.iter() {
8570 let mut start = selection.start;
8571 let mut end = selection.end;
8572 let is_entire_line = selection.is_empty();
8573 if is_entire_line {
8574 start = Point::new(start.row, 0);ˇ»
8575 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8576 }
8577 "#,
8578 );
8579 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8580 assert_eq!(
8581 cx.read_from_clipboard()
8582 .and_then(|item| item.text().as_deref().map(str::to_string)),
8583 Some(
8584 "in selections.iter() {
8585 let mut start = selection.start;
8586 let mut end = selection.end;
8587 let is_entire_line = selection.is_empty();
8588 if is_entire_line {
8589 start = Point::new(start.row, 0);"
8590 .to_string()
8591 ),
8592 "When selecting past the indent, the copying works as usual",
8593 );
8594 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8595 assert_eq!(
8596 cx.read_from_clipboard()
8597 .and_then(|item| item.text().as_deref().map(str::to_string)),
8598 Some(
8599 "in selections.iter() {
8600 let mut start = selection.start;
8601 let mut end = selection.end;
8602 let is_entire_line = selection.is_empty();
8603 if is_entire_line {
8604 start = Point::new(start.row, 0);"
8605 .to_string()
8606 ),
8607 "When selecting past the indent, nothing is trimmed"
8608 );
8609
8610 cx.set_state(
8611 r#" «for selection in selections.iter() {
8612 let mut start = selection.start;
8613
8614 let mut end = selection.end;
8615 let is_entire_line = selection.is_empty();
8616 if is_entire_line {
8617 start = Point::new(start.row, 0);
8618ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
8619 }
8620 "#,
8621 );
8622 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8623 assert_eq!(
8624 cx.read_from_clipboard()
8625 .and_then(|item| item.text().as_deref().map(str::to_string)),
8626 Some(
8627 "for selection in selections.iter() {
8628let mut start = selection.start;
8629
8630let mut end = selection.end;
8631let is_entire_line = selection.is_empty();
8632if is_entire_line {
8633 start = Point::new(start.row, 0);
8634"
8635 .to_string()
8636 ),
8637 "Copying with stripping should ignore empty lines"
8638 );
8639}
8640
8641#[gpui::test]
8642async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
8643 init_test(cx, |_| {});
8644
8645 let mut cx = EditorTestContext::new(cx).await;
8646
8647 cx.set_state(indoc! {"
8648 « fn main() {
8649 1
8650 }ˇ»
8651 "});
8652 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
8653 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
8654
8655 assert_eq!(
8656 cx.read_from_clipboard().and_then(|item| item.text()),
8657 Some("fn main() {\n 1\n}\n".to_string())
8658 );
8659
8660 let clipboard_selections: Vec<ClipboardSelection> = cx
8661 .read_from_clipboard()
8662 .and_then(|item| item.entries().first().cloned())
8663 .and_then(|entry| match entry {
8664 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8665 _ => None,
8666 })
8667 .expect("should have clipboard selections");
8668
8669 assert_eq!(clipboard_selections.len(), 1);
8670 assert!(clipboard_selections[0].is_entire_line);
8671
8672 cx.set_state(indoc! {"
8673 «fn main() {
8674 1
8675 }ˇ»
8676 "});
8677 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
8678 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
8679
8680 assert_eq!(
8681 cx.read_from_clipboard().and_then(|item| item.text()),
8682 Some("fn main() {\n 1\n}\n".to_string())
8683 );
8684
8685 let clipboard_selections: Vec<ClipboardSelection> = cx
8686 .read_from_clipboard()
8687 .and_then(|item| item.entries().first().cloned())
8688 .and_then(|entry| match entry {
8689 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8690 _ => None,
8691 })
8692 .expect("should have clipboard selections");
8693
8694 assert_eq!(clipboard_selections.len(), 1);
8695 assert!(clipboard_selections[0].is_entire_line);
8696}
8697
8698#[gpui::test]
8699async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
8700 init_test(cx, |_| {});
8701
8702 let fs = FakeFs::new(cx.executor());
8703 fs.insert_file(
8704 path!("/file.txt"),
8705 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
8706 )
8707 .await;
8708
8709 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
8710
8711 let buffer = project
8712 .update(cx, |project, cx| {
8713 project.open_local_buffer(path!("/file.txt"), cx)
8714 })
8715 .await
8716 .unwrap();
8717
8718 let multibuffer = cx.new(|cx| {
8719 let mut multibuffer = MultiBuffer::new(ReadWrite);
8720 multibuffer.set_excerpts_for_path(
8721 PathKey::sorted(0),
8722 buffer.clone(),
8723 [Point::new(2, 0)..Point::new(5, 0)],
8724 0,
8725 cx,
8726 );
8727 multibuffer
8728 });
8729
8730 let (editor, cx) = cx.add_window_view(|window, cx| {
8731 build_editor_with_project(project.clone(), multibuffer, window, cx)
8732 });
8733
8734 editor.update_in(cx, |editor, window, cx| {
8735 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
8736
8737 editor.select_all(&SelectAll, window, cx);
8738 editor.copy(&Copy, window, cx);
8739 });
8740
8741 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
8742 .read_from_clipboard()
8743 .and_then(|item| item.entries().first().cloned())
8744 .and_then(|entry| match entry {
8745 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8746 _ => None,
8747 });
8748
8749 let selections = clipboard_selections.expect("should have clipboard selections");
8750 assert_eq!(selections.len(), 1);
8751 let selection = &selections[0];
8752 assert_eq!(
8753 selection.line_range,
8754 Some(2..=5),
8755 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
8756 );
8757}
8758
8759#[gpui::test]
8760async fn test_paste_multiline(cx: &mut TestAppContext) {
8761 init_test(cx, |_| {});
8762
8763 let mut cx = EditorTestContext::new(cx).await;
8764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8765
8766 // Cut an indented block, without the leading whitespace.
8767 cx.set_state(indoc! {"
8768 const a: B = (
8769 c(),
8770 «d(
8771 e,
8772 f
8773 )ˇ»
8774 );
8775 "});
8776 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8777 cx.assert_editor_state(indoc! {"
8778 const a: B = (
8779 c(),
8780 ˇ
8781 );
8782 "});
8783
8784 // Paste it at the same position.
8785 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8786 cx.assert_editor_state(indoc! {"
8787 const a: B = (
8788 c(),
8789 d(
8790 e,
8791 f
8792 )ˇ
8793 );
8794 "});
8795
8796 // Paste it at a line with a lower indent level.
8797 cx.set_state(indoc! {"
8798 ˇ
8799 const a: B = (
8800 c(),
8801 );
8802 "});
8803 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8804 cx.assert_editor_state(indoc! {"
8805 d(
8806 e,
8807 f
8808 )ˇ
8809 const a: B = (
8810 c(),
8811 );
8812 "});
8813
8814 // Cut an indented block, with the leading whitespace.
8815 cx.set_state(indoc! {"
8816 const a: B = (
8817 c(),
8818 « d(
8819 e,
8820 f
8821 )
8822 ˇ»);
8823 "});
8824 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8825 cx.assert_editor_state(indoc! {"
8826 const a: B = (
8827 c(),
8828 ˇ);
8829 "});
8830
8831 // Paste it at the same position.
8832 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8833 cx.assert_editor_state(indoc! {"
8834 const a: B = (
8835 c(),
8836 d(
8837 e,
8838 f
8839 )
8840 ˇ);
8841 "});
8842
8843 // Paste it at a line with a higher indent level.
8844 cx.set_state(indoc! {"
8845 const a: B = (
8846 c(),
8847 d(
8848 e,
8849 fˇ
8850 )
8851 );
8852 "});
8853 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8854 cx.assert_editor_state(indoc! {"
8855 const a: B = (
8856 c(),
8857 d(
8858 e,
8859 f d(
8860 e,
8861 f
8862 )
8863 ˇ
8864 )
8865 );
8866 "});
8867
8868 // Copy an indented block, starting mid-line
8869 cx.set_state(indoc! {"
8870 const a: B = (
8871 c(),
8872 somethin«g(
8873 e,
8874 f
8875 )ˇ»
8876 );
8877 "});
8878 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8879
8880 // Paste it on a line with a lower indent level
8881 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
8882 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8883 cx.assert_editor_state(indoc! {"
8884 const a: B = (
8885 c(),
8886 something(
8887 e,
8888 f
8889 )
8890 );
8891 g(
8892 e,
8893 f
8894 )ˇ"});
8895}
8896
8897#[gpui::test]
8898async fn test_paste_undo_does_not_include_preceding_edits(cx: &mut TestAppContext) {
8899 init_test(cx, |_| {});
8900
8901 let mut cx = EditorTestContext::new(cx).await;
8902
8903 cx.update_editor(|e, _, cx| {
8904 e.buffer().update(cx, |buffer, cx| {
8905 buffer.set_group_interval(Duration::from_secs(10), cx)
8906 })
8907 });
8908 // Type some text
8909 cx.set_state("ˇ");
8910 cx.update_editor(|e, window, cx| e.insert("hello", window, cx));
8911 // cx.assert_editor_state("helloˇ");
8912
8913 // Paste some text immediately after typing
8914 cx.write_to_clipboard(ClipboardItem::new_string(" world".into()));
8915 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8916 cx.assert_editor_state("hello worldˇ");
8917
8918 // Undo should only undo the paste, not the preceding typing
8919 cx.update_editor(|e, window, cx| e.undo(&Undo, window, cx));
8920 cx.assert_editor_state("helloˇ");
8921
8922 // Undo again should undo the typing
8923 cx.update_editor(|e, window, cx| e.undo(&Undo, window, cx));
8924 cx.assert_editor_state("ˇ");
8925}
8926
8927#[gpui::test]
8928async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
8929 init_test(cx, |_| {});
8930
8931 cx.write_to_clipboard(ClipboardItem::new_string(
8932 " d(\n e\n );\n".into(),
8933 ));
8934
8935 let mut cx = EditorTestContext::new(cx).await;
8936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8937 cx.run_until_parked();
8938
8939 cx.set_state(indoc! {"
8940 fn a() {
8941 b();
8942 if c() {
8943 ˇ
8944 }
8945 }
8946 "});
8947
8948 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8949 cx.assert_editor_state(indoc! {"
8950 fn a() {
8951 b();
8952 if c() {
8953 d(
8954 e
8955 );
8956 ˇ
8957 }
8958 }
8959 "});
8960
8961 cx.set_state(indoc! {"
8962 fn a() {
8963 b();
8964 ˇ
8965 }
8966 "});
8967
8968 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8969 cx.assert_editor_state(indoc! {"
8970 fn a() {
8971 b();
8972 d(
8973 e
8974 );
8975 ˇ
8976 }
8977 "});
8978}
8979
8980#[gpui::test]
8981async fn test_paste_multiline_from_other_app_into_matching_cursors(cx: &mut TestAppContext) {
8982 init_test(cx, |_| {});
8983
8984 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
8985
8986 let mut cx = EditorTestContext::new(cx).await;
8987
8988 // Paste into 3 cursors: each cursor should receive one line.
8989 cx.set_state("ˇ one ˇ two ˇ three");
8990 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8991 cx.assert_editor_state("alphaˇ one betaˇ two gammaˇ three");
8992
8993 // Paste into 2 cursors: line count doesn't match, so paste entire text at each cursor.
8994 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
8995 cx.set_state("ˇ one ˇ two");
8996 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8997 cx.assert_editor_state("alpha\nbeta\ngammaˇ one alpha\nbeta\ngammaˇ two");
8998
8999 // Paste into a single cursor: should paste everything as-is.
9000 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
9001 cx.set_state("ˇ one");
9002 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
9003 cx.assert_editor_state("alpha\nbeta\ngammaˇ one");
9004
9005 // Paste with selections: each selection is replaced with its corresponding line.
9006 cx.write_to_clipboard(ClipboardItem::new_string("xx\nyy\nzz".into()));
9007 cx.set_state("«aˇ» one «bˇ» two «cˇ» three");
9008 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
9009 cx.assert_editor_state("xxˇ one yyˇ two zzˇ three");
9010}
9011
9012#[gpui::test]
9013fn test_select_all(cx: &mut TestAppContext) {
9014 init_test(cx, |_| {});
9015
9016 let editor = cx.add_window(|window, cx| {
9017 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
9018 build_editor(buffer, window, cx)
9019 });
9020 _ = editor.update(cx, |editor, window, cx| {
9021 editor.select_all(&SelectAll, window, cx);
9022 assert_eq!(
9023 display_ranges(editor, cx),
9024 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
9025 );
9026 });
9027}
9028
9029#[gpui::test]
9030fn test_select_line(cx: &mut TestAppContext) {
9031 init_test(cx, |_| {});
9032
9033 let editor = cx.add_window(|window, cx| {
9034 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
9035 build_editor(buffer, window, cx)
9036 });
9037 _ = editor.update(cx, |editor, window, cx| {
9038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9039 s.select_display_ranges([
9040 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9041 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
9042 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
9043 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
9044 ])
9045 });
9046 editor.select_line(&SelectLine, window, cx);
9047 // Adjacent line selections should NOT merge (only overlapping ones do)
9048 assert_eq!(
9049 display_ranges(editor, cx),
9050 vec![
9051 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
9052 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
9053 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
9054 ]
9055 );
9056 });
9057
9058 _ = editor.update(cx, |editor, window, cx| {
9059 editor.select_line(&SelectLine, window, cx);
9060 assert_eq!(
9061 display_ranges(editor, cx),
9062 vec![
9063 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
9064 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
9065 ]
9066 );
9067 });
9068
9069 _ = editor.update(cx, |editor, window, cx| {
9070 editor.select_line(&SelectLine, window, cx);
9071 // Adjacent but not overlapping, so they stay separate
9072 assert_eq!(
9073 display_ranges(editor, cx),
9074 vec![
9075 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
9076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
9077 ]
9078 );
9079 });
9080}
9081
9082#[gpui::test]
9083async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
9084 init_test(cx, |_| {});
9085 let mut cx = EditorTestContext::new(cx).await;
9086
9087 #[track_caller]
9088 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
9089 cx.set_state(initial_state);
9090 cx.update_editor(|e, window, cx| {
9091 e.split_selection_into_lines(&Default::default(), window, cx)
9092 });
9093 cx.assert_editor_state(expected_state);
9094 }
9095
9096 // Selection starts and ends at the middle of lines, left-to-right
9097 test(
9098 &mut cx,
9099 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
9100 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
9101 );
9102 // Same thing, right-to-left
9103 test(
9104 &mut cx,
9105 "aa\nb«b\ncc\ndd\neˇ»e\nff",
9106 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
9107 );
9108
9109 // Whole buffer, left-to-right, last line *doesn't* end with newline
9110 test(
9111 &mut cx,
9112 "«ˇaa\nbb\ncc\ndd\nee\nff»",
9113 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
9114 );
9115 // Same thing, right-to-left
9116 test(
9117 &mut cx,
9118 "«aa\nbb\ncc\ndd\nee\nffˇ»",
9119 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
9120 );
9121
9122 // Whole buffer, left-to-right, last line ends with newline
9123 test(
9124 &mut cx,
9125 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
9126 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
9127 );
9128 // Same thing, right-to-left
9129 test(
9130 &mut cx,
9131 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
9132 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
9133 );
9134
9135 // Starts at the end of a line, ends at the start of another
9136 test(
9137 &mut cx,
9138 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
9139 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
9140 );
9141}
9142
9143#[gpui::test]
9144async fn test_split_selection_into_lines_does_not_scroll(cx: &mut TestAppContext) {
9145 init_test(cx, |_| {});
9146 let mut cx = EditorTestContext::new(cx).await;
9147
9148 let large_body = "\nline".repeat(300);
9149 cx.set_state(&format!("«ˇstart{large_body}\nend»"));
9150 let initial_scroll_position = cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
9151
9152 cx.update_editor(|editor, window, cx| {
9153 editor.split_selection_into_lines(&Default::default(), window, cx);
9154 });
9155
9156 let scroll_position_after_split = cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
9157 assert_eq!(
9158 initial_scroll_position, scroll_position_after_split,
9159 "Scroll position should not change after splitting selection into lines"
9160 );
9161}
9162
9163#[gpui::test]
9164async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
9165 init_test(cx, |_| {});
9166
9167 let editor = cx.add_window(|window, cx| {
9168 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
9169 build_editor(buffer, window, cx)
9170 });
9171
9172 // setup
9173 _ = editor.update(cx, |editor, window, cx| {
9174 editor.fold_creases(
9175 vec![
9176 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
9177 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
9178 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
9179 ],
9180 true,
9181 window,
9182 cx,
9183 );
9184 assert_eq!(
9185 editor.display_text(cx),
9186 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
9187 );
9188 });
9189
9190 _ = editor.update(cx, |editor, window, cx| {
9191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9192 s.select_display_ranges([
9193 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9194 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
9195 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
9196 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
9197 ])
9198 });
9199 editor.split_selection_into_lines(&Default::default(), window, cx);
9200 assert_eq!(
9201 editor.display_text(cx),
9202 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
9203 );
9204 });
9205 EditorTestContext::for_editor(editor, cx)
9206 .await
9207 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
9208
9209 _ = editor.update(cx, |editor, window, cx| {
9210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9211 s.select_display_ranges([
9212 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
9213 ])
9214 });
9215 editor.split_selection_into_lines(&Default::default(), window, cx);
9216 assert_eq!(
9217 editor.display_text(cx),
9218 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
9219 );
9220 assert_eq!(
9221 display_ranges(editor, cx),
9222 [
9223 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
9224 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
9225 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
9226 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
9227 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
9228 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
9229 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
9230 ]
9231 );
9232 });
9233 EditorTestContext::for_editor(editor, cx)
9234 .await
9235 .assert_editor_state(
9236 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
9237 );
9238}
9239
9240#[gpui::test]
9241async fn test_add_selection_above_below(cx: &mut TestAppContext) {
9242 init_test(cx, |_| {});
9243
9244 let mut cx = EditorTestContext::new(cx).await;
9245
9246 cx.set_state(indoc!(
9247 r#"abc
9248 defˇghi
9249
9250 jk
9251 nlmo
9252 "#
9253 ));
9254
9255 cx.update_editor(|editor, window, cx| {
9256 editor.add_selection_above(&Default::default(), window, cx);
9257 });
9258
9259 cx.assert_editor_state(indoc!(
9260 r#"abcˇ
9261 defˇghi
9262
9263 jk
9264 nlmo
9265 "#
9266 ));
9267
9268 cx.update_editor(|editor, window, cx| {
9269 editor.add_selection_above(&Default::default(), window, cx);
9270 });
9271
9272 cx.assert_editor_state(indoc!(
9273 r#"abcˇ
9274 defˇghi
9275
9276 jk
9277 nlmo
9278 "#
9279 ));
9280
9281 cx.update_editor(|editor, window, cx| {
9282 editor.add_selection_below(&Default::default(), window, cx);
9283 });
9284
9285 cx.assert_editor_state(indoc!(
9286 r#"abc
9287 defˇghi
9288
9289 jk
9290 nlmo
9291 "#
9292 ));
9293
9294 cx.update_editor(|editor, window, cx| {
9295 editor.undo_selection(&Default::default(), window, cx);
9296 });
9297
9298 cx.assert_editor_state(indoc!(
9299 r#"abcˇ
9300 defˇghi
9301
9302 jk
9303 nlmo
9304 "#
9305 ));
9306
9307 cx.update_editor(|editor, window, cx| {
9308 editor.redo_selection(&Default::default(), window, cx);
9309 });
9310
9311 cx.assert_editor_state(indoc!(
9312 r#"abc
9313 defˇghi
9314
9315 jk
9316 nlmo
9317 "#
9318 ));
9319
9320 cx.update_editor(|editor, window, cx| {
9321 editor.add_selection_below(&Default::default(), window, cx);
9322 });
9323
9324 cx.assert_editor_state(indoc!(
9325 r#"abc
9326 defˇghi
9327 ˇ
9328 jk
9329 nlmo
9330 "#
9331 ));
9332
9333 cx.update_editor(|editor, window, cx| {
9334 editor.add_selection_below(&Default::default(), window, cx);
9335 });
9336
9337 cx.assert_editor_state(indoc!(
9338 r#"abc
9339 defˇghi
9340 ˇ
9341 jkˇ
9342 nlmo
9343 "#
9344 ));
9345
9346 cx.update_editor(|editor, window, cx| {
9347 editor.add_selection_below(&Default::default(), window, cx);
9348 });
9349
9350 cx.assert_editor_state(indoc!(
9351 r#"abc
9352 defˇghi
9353 ˇ
9354 jkˇ
9355 nlmˇo
9356 "#
9357 ));
9358
9359 cx.update_editor(|editor, window, cx| {
9360 editor.add_selection_below(&Default::default(), window, cx);
9361 });
9362
9363 cx.assert_editor_state(indoc!(
9364 r#"abc
9365 defˇghi
9366 ˇ
9367 jkˇ
9368 nlmˇo
9369 ˇ"#
9370 ));
9371
9372 // change selections
9373 cx.set_state(indoc!(
9374 r#"abc
9375 def«ˇg»hi
9376
9377 jk
9378 nlmo
9379 "#
9380 ));
9381
9382 cx.update_editor(|editor, window, cx| {
9383 editor.add_selection_below(&Default::default(), window, cx);
9384 });
9385
9386 cx.assert_editor_state(indoc!(
9387 r#"abc
9388 def«ˇg»hi
9389
9390 jk
9391 nlm«ˇo»
9392 "#
9393 ));
9394
9395 cx.update_editor(|editor, window, cx| {
9396 editor.add_selection_below(&Default::default(), window, cx);
9397 });
9398
9399 cx.assert_editor_state(indoc!(
9400 r#"abc
9401 def«ˇg»hi
9402
9403 jk
9404 nlm«ˇo»
9405 "#
9406 ));
9407
9408 cx.update_editor(|editor, window, cx| {
9409 editor.add_selection_above(&Default::default(), window, cx);
9410 });
9411
9412 cx.assert_editor_state(indoc!(
9413 r#"abc
9414 def«ˇg»hi
9415
9416 jk
9417 nlmo
9418 "#
9419 ));
9420
9421 cx.update_editor(|editor, window, cx| {
9422 editor.add_selection_above(&Default::default(), window, cx);
9423 });
9424
9425 cx.assert_editor_state(indoc!(
9426 r#"abc
9427 def«ˇg»hi
9428
9429 jk
9430 nlmo
9431 "#
9432 ));
9433
9434 // Change selections again
9435 cx.set_state(indoc!(
9436 r#"a«bc
9437 defgˇ»hi
9438
9439 jk
9440 nlmo
9441 "#
9442 ));
9443
9444 cx.update_editor(|editor, window, cx| {
9445 editor.add_selection_below(&Default::default(), window, cx);
9446 });
9447
9448 cx.assert_editor_state(indoc!(
9449 r#"a«bcˇ»
9450 d«efgˇ»hi
9451
9452 j«kˇ»
9453 nlmo
9454 "#
9455 ));
9456
9457 cx.update_editor(|editor, window, cx| {
9458 editor.add_selection_below(&Default::default(), window, cx);
9459 });
9460 cx.assert_editor_state(indoc!(
9461 r#"a«bcˇ»
9462 d«efgˇ»hi
9463
9464 j«kˇ»
9465 n«lmoˇ»
9466 "#
9467 ));
9468 cx.update_editor(|editor, window, cx| {
9469 editor.add_selection_above(&Default::default(), window, cx);
9470 });
9471
9472 cx.assert_editor_state(indoc!(
9473 r#"a«bcˇ»
9474 d«efgˇ»hi
9475
9476 j«kˇ»
9477 nlmo
9478 "#
9479 ));
9480
9481 // Change selections again
9482 cx.set_state(indoc!(
9483 r#"abc
9484 d«ˇefghi
9485
9486 jk
9487 nlm»o
9488 "#
9489 ));
9490
9491 cx.update_editor(|editor, window, cx| {
9492 editor.add_selection_above(&Default::default(), window, cx);
9493 });
9494
9495 cx.assert_editor_state(indoc!(
9496 r#"a«ˇbc»
9497 d«ˇef»ghi
9498
9499 j«ˇk»
9500 n«ˇlm»o
9501 "#
9502 ));
9503
9504 cx.update_editor(|editor, window, cx| {
9505 editor.add_selection_below(&Default::default(), window, cx);
9506 });
9507
9508 cx.assert_editor_state(indoc!(
9509 r#"abc
9510 d«ˇef»ghi
9511
9512 j«ˇk»
9513 n«ˇlm»o
9514 "#
9515 ));
9516
9517 // Assert that the oldest selection's goal column is used when adding more
9518 // selections, not the most recently added selection's actual column.
9519 cx.set_state(indoc! {"
9520 foo bar bazˇ
9521 foo
9522 foo bar
9523 "});
9524
9525 cx.update_editor(|editor, window, cx| {
9526 editor.add_selection_below(
9527 &AddSelectionBelow {
9528 skip_soft_wrap: true,
9529 },
9530 window,
9531 cx,
9532 );
9533 });
9534
9535 cx.assert_editor_state(indoc! {"
9536 foo bar bazˇ
9537 fooˇ
9538 foo bar
9539 "});
9540
9541 cx.update_editor(|editor, window, cx| {
9542 editor.add_selection_below(
9543 &AddSelectionBelow {
9544 skip_soft_wrap: true,
9545 },
9546 window,
9547 cx,
9548 );
9549 });
9550
9551 cx.assert_editor_state(indoc! {"
9552 foo bar bazˇ
9553 fooˇ
9554 foo barˇ
9555 "});
9556
9557 cx.set_state(indoc! {"
9558 foo bar baz
9559 foo
9560 foo barˇ
9561 "});
9562
9563 cx.update_editor(|editor, window, cx| {
9564 editor.add_selection_above(
9565 &AddSelectionAbove {
9566 skip_soft_wrap: true,
9567 },
9568 window,
9569 cx,
9570 );
9571 });
9572
9573 cx.assert_editor_state(indoc! {"
9574 foo bar baz
9575 fooˇ
9576 foo barˇ
9577 "});
9578
9579 cx.update_editor(|editor, window, cx| {
9580 editor.add_selection_above(
9581 &AddSelectionAbove {
9582 skip_soft_wrap: true,
9583 },
9584 window,
9585 cx,
9586 );
9587 });
9588
9589 cx.assert_editor_state(indoc! {"
9590 foo barˇ baz
9591 fooˇ
9592 foo barˇ
9593 "});
9594}
9595
9596#[gpui::test]
9597async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
9598 init_test(cx, |_| {});
9599 let mut cx = EditorTestContext::new(cx).await;
9600
9601 cx.set_state(indoc!(
9602 r#"line onˇe
9603 liˇne two
9604 line three
9605 line four"#
9606 ));
9607
9608 cx.update_editor(|editor, window, cx| {
9609 editor.add_selection_below(&Default::default(), window, cx);
9610 });
9611
9612 // test multiple cursors expand in the same direction
9613 cx.assert_editor_state(indoc!(
9614 r#"line onˇe
9615 liˇne twˇo
9616 liˇne three
9617 line four"#
9618 ));
9619
9620 cx.update_editor(|editor, window, cx| {
9621 editor.add_selection_below(&Default::default(), window, cx);
9622 });
9623
9624 cx.update_editor(|editor, window, cx| {
9625 editor.add_selection_below(&Default::default(), window, cx);
9626 });
9627
9628 // test multiple cursors expand below overflow
9629 cx.assert_editor_state(indoc!(
9630 r#"line onˇe
9631 liˇne twˇo
9632 liˇne thˇree
9633 liˇne foˇur"#
9634 ));
9635
9636 cx.update_editor(|editor, window, cx| {
9637 editor.add_selection_above(&Default::default(), window, cx);
9638 });
9639
9640 // test multiple cursors retrieves back correctly
9641 cx.assert_editor_state(indoc!(
9642 r#"line onˇe
9643 liˇne twˇo
9644 liˇne thˇree
9645 line four"#
9646 ));
9647
9648 cx.update_editor(|editor, window, cx| {
9649 editor.add_selection_above(&Default::default(), window, cx);
9650 });
9651
9652 cx.update_editor(|editor, window, cx| {
9653 editor.add_selection_above(&Default::default(), window, cx);
9654 });
9655
9656 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
9657 cx.assert_editor_state(indoc!(
9658 r#"liˇne onˇe
9659 liˇne two
9660 line three
9661 line four"#
9662 ));
9663
9664 cx.update_editor(|editor, window, cx| {
9665 editor.undo_selection(&Default::default(), window, cx);
9666 });
9667
9668 // test undo
9669 cx.assert_editor_state(indoc!(
9670 r#"line onˇe
9671 liˇne twˇo
9672 line three
9673 line four"#
9674 ));
9675
9676 cx.update_editor(|editor, window, cx| {
9677 editor.redo_selection(&Default::default(), window, cx);
9678 });
9679
9680 // test redo
9681 cx.assert_editor_state(indoc!(
9682 r#"liˇne onˇe
9683 liˇne two
9684 line three
9685 line four"#
9686 ));
9687
9688 cx.set_state(indoc!(
9689 r#"abcd
9690 ef«ghˇ»
9691 ijkl
9692 «mˇ»nop"#
9693 ));
9694
9695 cx.update_editor(|editor, window, cx| {
9696 editor.add_selection_above(&Default::default(), window, cx);
9697 });
9698
9699 // test multiple selections expand in the same direction
9700 cx.assert_editor_state(indoc!(
9701 r#"ab«cdˇ»
9702 ef«ghˇ»
9703 «iˇ»jkl
9704 «mˇ»nop"#
9705 ));
9706
9707 cx.update_editor(|editor, window, cx| {
9708 editor.add_selection_above(&Default::default(), window, cx);
9709 });
9710
9711 // test multiple selection upward overflow
9712 cx.assert_editor_state(indoc!(
9713 r#"ab«cdˇ»
9714 «eˇ»f«ghˇ»
9715 «iˇ»jkl
9716 «mˇ»nop"#
9717 ));
9718
9719 cx.update_editor(|editor, window, cx| {
9720 editor.add_selection_below(&Default::default(), window, cx);
9721 });
9722
9723 // test multiple selection retrieves back correctly
9724 cx.assert_editor_state(indoc!(
9725 r#"abcd
9726 ef«ghˇ»
9727 «iˇ»jkl
9728 «mˇ»nop"#
9729 ));
9730
9731 cx.update_editor(|editor, window, cx| {
9732 editor.add_selection_below(&Default::default(), window, cx);
9733 });
9734
9735 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
9736 cx.assert_editor_state(indoc!(
9737 r#"abcd
9738 ef«ghˇ»
9739 ij«klˇ»
9740 «mˇ»nop"#
9741 ));
9742
9743 cx.update_editor(|editor, window, cx| {
9744 editor.undo_selection(&Default::default(), window, cx);
9745 });
9746
9747 // test undo
9748 cx.assert_editor_state(indoc!(
9749 r#"abcd
9750 ef«ghˇ»
9751 «iˇ»jkl
9752 «mˇ»nop"#
9753 ));
9754
9755 cx.update_editor(|editor, window, cx| {
9756 editor.redo_selection(&Default::default(), window, cx);
9757 });
9758
9759 // test redo
9760 cx.assert_editor_state(indoc!(
9761 r#"abcd
9762 ef«ghˇ»
9763 ij«klˇ»
9764 «mˇ»nop"#
9765 ));
9766}
9767
9768#[gpui::test]
9769async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
9770 init_test(cx, |_| {});
9771 let mut cx = EditorTestContext::new(cx).await;
9772
9773 cx.set_state(indoc!(
9774 r#"line onˇe
9775 liˇne two
9776 line three
9777 line four"#
9778 ));
9779
9780 cx.update_editor(|editor, window, cx| {
9781 editor.add_selection_below(&Default::default(), window, cx);
9782 editor.add_selection_below(&Default::default(), window, cx);
9783 editor.add_selection_below(&Default::default(), window, cx);
9784 });
9785
9786 // initial state with two multi cursor groups
9787 cx.assert_editor_state(indoc!(
9788 r#"line onˇe
9789 liˇne twˇo
9790 liˇne thˇree
9791 liˇne foˇur"#
9792 ));
9793
9794 // add single cursor in middle - simulate opt click
9795 cx.update_editor(|editor, window, cx| {
9796 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
9797 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9798 editor.end_selection(window, cx);
9799 });
9800
9801 cx.assert_editor_state(indoc!(
9802 r#"line onˇe
9803 liˇne twˇo
9804 liˇneˇ thˇree
9805 liˇne foˇur"#
9806 ));
9807
9808 cx.update_editor(|editor, window, cx| {
9809 editor.add_selection_above(&Default::default(), window, cx);
9810 });
9811
9812 // test new added selection expands above and existing selection shrinks
9813 cx.assert_editor_state(indoc!(
9814 r#"line onˇe
9815 liˇneˇ twˇo
9816 liˇneˇ thˇree
9817 line four"#
9818 ));
9819
9820 cx.update_editor(|editor, window, cx| {
9821 editor.add_selection_above(&Default::default(), window, cx);
9822 });
9823
9824 // test new added selection expands above and existing selection shrinks
9825 cx.assert_editor_state(indoc!(
9826 r#"lineˇ onˇe
9827 liˇneˇ twˇo
9828 lineˇ three
9829 line four"#
9830 ));
9831
9832 // intial state with two selection groups
9833 cx.set_state(indoc!(
9834 r#"abcd
9835 ef«ghˇ»
9836 ijkl
9837 «mˇ»nop"#
9838 ));
9839
9840 cx.update_editor(|editor, window, cx| {
9841 editor.add_selection_above(&Default::default(), window, cx);
9842 editor.add_selection_above(&Default::default(), window, cx);
9843 });
9844
9845 cx.assert_editor_state(indoc!(
9846 r#"ab«cdˇ»
9847 «eˇ»f«ghˇ»
9848 «iˇ»jkl
9849 «mˇ»nop"#
9850 ));
9851
9852 // add single selection in middle - simulate opt drag
9853 cx.update_editor(|editor, window, cx| {
9854 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
9855 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9856 editor.update_selection(
9857 DisplayPoint::new(DisplayRow(2), 4),
9858 0,
9859 gpui::Point::<f32>::default(),
9860 window,
9861 cx,
9862 );
9863 editor.end_selection(window, cx);
9864 });
9865
9866 cx.assert_editor_state(indoc!(
9867 r#"ab«cdˇ»
9868 «eˇ»f«ghˇ»
9869 «iˇ»jk«lˇ»
9870 «mˇ»nop"#
9871 ));
9872
9873 cx.update_editor(|editor, window, cx| {
9874 editor.add_selection_below(&Default::default(), window, cx);
9875 });
9876
9877 // test new added selection expands below, others shrinks from above
9878 cx.assert_editor_state(indoc!(
9879 r#"abcd
9880 ef«ghˇ»
9881 «iˇ»jk«lˇ»
9882 «mˇ»no«pˇ»"#
9883 ));
9884}
9885
9886#[gpui::test]
9887async fn test_add_selection_above_below_multibyte(cx: &mut TestAppContext) {
9888 init_test(cx, |_| {});
9889 let mut cx = EditorTestContext::new(cx).await;
9890
9891 // Cursor after "Häl" (byte column 4, char column 3) should align to
9892 // char column 3 on the ASCII line below, not byte column 4.
9893 cx.set_state(indoc!(
9894 r#"Hälˇlö
9895 Hallo"#
9896 ));
9897
9898 cx.update_editor(|editor, window, cx| {
9899 editor.add_selection_below(&Default::default(), window, cx);
9900 });
9901
9902 cx.assert_editor_state(indoc!(
9903 r#"Hälˇlö
9904 Halˇlo"#
9905 ));
9906}
9907
9908#[gpui::test]
9909async fn test_select_next(cx: &mut TestAppContext) {
9910 init_test(cx, |_| {});
9911 let mut cx = EditorTestContext::new(cx).await;
9912
9913 // Enable case sensitive search.
9914 update_test_editor_settings(&mut cx, &|settings| {
9915 let mut search_settings = SearchSettingsContent::default();
9916 search_settings.case_sensitive = Some(true);
9917 settings.search = Some(search_settings);
9918 });
9919
9920 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9921
9922 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9923 .unwrap();
9924 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9925
9926 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9927 .unwrap();
9928 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9929
9930 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9931 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9932
9933 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9934 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9935
9936 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9937 .unwrap();
9938 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9939
9940 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9941 .unwrap();
9942 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9943
9944 // Test selection direction should be preserved
9945 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9946
9947 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9948 .unwrap();
9949 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
9950
9951 // Test case sensitivity
9952 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
9953 cx.update_editor(|e, window, cx| {
9954 e.select_next(&SelectNext::default(), window, cx).unwrap();
9955 });
9956 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9957
9958 // Disable case sensitive search.
9959 update_test_editor_settings(&mut cx, &|settings| {
9960 let mut search_settings = SearchSettingsContent::default();
9961 search_settings.case_sensitive = Some(false);
9962 settings.search = Some(search_settings);
9963 });
9964
9965 cx.set_state("«ˇfoo»\nFOO\nFoo");
9966 cx.update_editor(|e, window, cx| {
9967 e.select_next(&SelectNext::default(), window, cx).unwrap();
9968 e.select_next(&SelectNext::default(), window, cx).unwrap();
9969 });
9970 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9971}
9972
9973#[gpui::test]
9974async fn test_select_all_matches(cx: &mut TestAppContext) {
9975 init_test(cx, |_| {});
9976 let mut cx = EditorTestContext::new(cx).await;
9977
9978 // Enable case sensitive search.
9979 update_test_editor_settings(&mut cx, &|settings| {
9980 let mut search_settings = SearchSettingsContent::default();
9981 search_settings.case_sensitive = Some(true);
9982 settings.search = Some(search_settings);
9983 });
9984
9985 // Test caret-only selections
9986 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9987 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9988 .unwrap();
9989 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9990
9991 // Test left-to-right selections
9992 cx.set_state("abc\n«abcˇ»\nabc");
9993 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9994 .unwrap();
9995 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
9996
9997 // Test right-to-left selections
9998 cx.set_state("abc\n«ˇabc»\nabc");
9999 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
10000 .unwrap();
10001 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
10002
10003 // Test selecting whitespace with caret selection
10004 cx.set_state("abc\nˇ abc\nabc");
10005 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
10006 .unwrap();
10007 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
10008
10009 // Test selecting whitespace with left-to-right selection
10010 cx.set_state("abc\n«ˇ »abc\nabc");
10011 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
10012 .unwrap();
10013 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
10014
10015 // Test no matches with right-to-left selection
10016 cx.set_state("abc\n« ˇ»abc\nabc");
10017 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
10018 .unwrap();
10019 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
10020
10021 // Test with a single word and clip_at_line_ends=true (#29823)
10022 cx.set_state("aˇbc");
10023 cx.update_editor(|e, window, cx| {
10024 e.set_clip_at_line_ends(true, cx);
10025 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
10026 e.set_clip_at_line_ends(false, cx);
10027 });
10028 cx.assert_editor_state("«abcˇ»");
10029
10030 // Test case sensitivity
10031 cx.set_state("fˇoo\nFOO\nFoo");
10032 cx.update_editor(|e, window, cx| {
10033 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
10034 });
10035 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
10036
10037 // Disable case sensitive search.
10038 update_test_editor_settings(&mut cx, &|settings| {
10039 let mut search_settings = SearchSettingsContent::default();
10040 search_settings.case_sensitive = Some(false);
10041 settings.search = Some(search_settings);
10042 });
10043
10044 cx.set_state("fˇoo\nFOO\nFoo");
10045 cx.update_editor(|e, window, cx| {
10046 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
10047 });
10048 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
10049}
10050
10051#[gpui::test]
10052async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
10053 init_test(cx, |_| {});
10054
10055 let mut cx = EditorTestContext::new(cx).await;
10056 let large_body_1 = "\nd".repeat(200);
10057 let large_body_2 = "\ne".repeat(200);
10058
10059 cx.set_state(&format!(
10060 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
10061 ));
10062 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
10063 let scroll_position = editor.scroll_position(cx);
10064 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
10065 scroll_position
10066 });
10067
10068 cx.update_editor(|editor, window, cx| editor.select_all_matches(&SelectAllMatches, window, cx))
10069 .unwrap();
10070 cx.assert_editor_state(&format!(
10071 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
10072 ));
10073 cx.update_editor(|editor, _, cx| {
10074 assert_eq!(
10075 editor.scroll_position(cx),
10076 initial_scroll_position,
10077 "Scroll position should not change after selecting all matches"
10078 )
10079 });
10080
10081 // Simulate typing while the selections are active, as that is where the
10082 // editor would attempt to actually scroll to the newest selection, which
10083 // should have been set as the original selection to avoid scrolling to the
10084 // last match.
10085 cx.simulate_keystroke("x");
10086 cx.update_editor(|editor, _, cx| {
10087 assert_eq!(
10088 editor.scroll_position(cx),
10089 initial_scroll_position,
10090 "Scroll position should not change after editing all matches"
10091 )
10092 });
10093
10094 cx.set_state(&format!(
10095 "abc\nabc{large_body_1} «aˇ»bc{large_body_2}\nefabc\nabc"
10096 ));
10097 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
10098 let scroll_position = editor.scroll_position(cx);
10099 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
10100 scroll_position
10101 });
10102
10103 cx.update_editor(|editor, window, cx| editor.select_all_matches(&SelectAllMatches, window, cx))
10104 .unwrap();
10105 cx.assert_editor_state(&format!(
10106 "«aˇ»bc\n«aˇ»bc{large_body_1} «aˇ»bc{large_body_2}\nef«aˇ»bc\n«aˇ»bc"
10107 ));
10108 cx.update_editor(|editor, _, cx| {
10109 assert_eq!(
10110 editor.scroll_position(cx),
10111 initial_scroll_position,
10112 "Scroll position should not change after selecting all matches"
10113 )
10114 });
10115
10116 cx.simulate_keystroke("x");
10117 cx.update_editor(|editor, _, cx| {
10118 assert_eq!(
10119 editor.scroll_position(cx),
10120 initial_scroll_position,
10121 "Scroll position should not change after editing all matches"
10122 )
10123 });
10124}
10125
10126#[gpui::test]
10127async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
10128 init_test(cx, |_| {});
10129
10130 let mut cx = EditorLspTestContext::new_rust(
10131 lsp::ServerCapabilities {
10132 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10133 ..Default::default()
10134 },
10135 cx,
10136 )
10137 .await;
10138
10139 cx.set_state(indoc! {"
10140 line 1
10141 line 2
10142 linˇe 3
10143 line 4
10144 line 5
10145 "});
10146
10147 // Make an edit
10148 cx.update_editor(|editor, window, cx| {
10149 editor.handle_input("X", window, cx);
10150 });
10151
10152 // Move cursor to a different position
10153 cx.update_editor(|editor, window, cx| {
10154 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10155 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
10156 });
10157 });
10158
10159 cx.assert_editor_state(indoc! {"
10160 line 1
10161 line 2
10162 linXe 3
10163 line 4
10164 liˇne 5
10165 "});
10166
10167 cx.lsp
10168 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
10169 Ok(Some(vec![lsp::TextEdit::new(
10170 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10171 "PREFIX ".to_string(),
10172 )]))
10173 });
10174
10175 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
10176 .unwrap()
10177 .await
10178 .unwrap();
10179
10180 cx.assert_editor_state(indoc! {"
10181 PREFIX line 1
10182 line 2
10183 linXe 3
10184 line 4
10185 liˇne 5
10186 "});
10187
10188 // Undo formatting
10189 cx.update_editor(|editor, window, cx| {
10190 editor.undo(&Default::default(), window, cx);
10191 });
10192
10193 // Verify cursor moved back to position after edit
10194 cx.assert_editor_state(indoc! {"
10195 line 1
10196 line 2
10197 linXˇe 3
10198 line 4
10199 line 5
10200 "});
10201}
10202
10203#[gpui::test]
10204async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
10205 init_test(cx, |_| {});
10206
10207 let mut cx = EditorTestContext::new(cx).await;
10208
10209 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
10210 cx.update_editor(|editor, window, cx| {
10211 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
10212 });
10213
10214 cx.set_state(indoc! {"
10215 line 1
10216 line 2
10217 linˇe 3
10218 line 4
10219 line 5
10220 line 6
10221 line 7
10222 line 8
10223 line 9
10224 line 10
10225 "});
10226
10227 let snapshot = cx.buffer_snapshot();
10228 let edit_position = snapshot.anchor_after(Point::new(2, 4));
10229
10230 cx.update(|_, cx| {
10231 provider.update(cx, |provider, _| {
10232 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
10233 id: None,
10234 edits: vec![(edit_position..edit_position, "X".into())],
10235 cursor_position: None,
10236 edit_preview: None,
10237 }))
10238 })
10239 });
10240
10241 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
10242 cx.update_editor(|editor, window, cx| {
10243 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
10244 });
10245
10246 cx.assert_editor_state(indoc! {"
10247 line 1
10248 line 2
10249 lineXˇ 3
10250 line 4
10251 line 5
10252 line 6
10253 line 7
10254 line 8
10255 line 9
10256 line 10
10257 "});
10258
10259 cx.update_editor(|editor, window, cx| {
10260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10261 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
10262 });
10263 });
10264
10265 cx.assert_editor_state(indoc! {"
10266 line 1
10267 line 2
10268 lineX 3
10269 line 4
10270 line 5
10271 line 6
10272 line 7
10273 line 8
10274 line 9
10275 liˇne 10
10276 "});
10277
10278 cx.update_editor(|editor, window, cx| {
10279 editor.undo(&Default::default(), window, cx);
10280 });
10281
10282 cx.assert_editor_state(indoc! {"
10283 line 1
10284 line 2
10285 lineˇ 3
10286 line 4
10287 line 5
10288 line 6
10289 line 7
10290 line 8
10291 line 9
10292 line 10
10293 "});
10294}
10295
10296#[gpui::test]
10297async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
10298 init_test(cx, |_| {});
10299
10300 let mut cx = EditorTestContext::new(cx).await;
10301 cx.set_state(
10302 r#"let foo = 2;
10303lˇet foo = 2;
10304let fooˇ = 2;
10305let foo = 2;
10306let foo = ˇ2;"#,
10307 );
10308
10309 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
10310 .unwrap();
10311 cx.assert_editor_state(
10312 r#"let foo = 2;
10313«letˇ» foo = 2;
10314let «fooˇ» = 2;
10315let foo = 2;
10316let foo = «2ˇ»;"#,
10317 );
10318
10319 // noop for multiple selections with different contents
10320 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
10321 .unwrap();
10322 cx.assert_editor_state(
10323 r#"let foo = 2;
10324«letˇ» foo = 2;
10325let «fooˇ» = 2;
10326let foo = 2;
10327let foo = «2ˇ»;"#,
10328 );
10329
10330 // Test last selection direction should be preserved
10331 cx.set_state(
10332 r#"let foo = 2;
10333let foo = 2;
10334let «fooˇ» = 2;
10335let «ˇfoo» = 2;
10336let foo = 2;"#,
10337 );
10338
10339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
10340 .unwrap();
10341 cx.assert_editor_state(
10342 r#"let foo = 2;
10343let foo = 2;
10344let «fooˇ» = 2;
10345let «ˇfoo» = 2;
10346let «ˇfoo» = 2;"#,
10347 );
10348}
10349
10350#[gpui::test]
10351async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
10352 init_test(cx, |_| {});
10353
10354 let mut cx =
10355 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc»\nddd", "aaa\n«bbb\nccc»\nddd"]);
10356
10357 cx.assert_editor_state(indoc! {"
10358 ˇbbb
10359 ccc
10360 bbb
10361 ccc"});
10362 cx.dispatch_action(SelectPrevious::default());
10363 cx.assert_editor_state(indoc! {"
10364 «bbbˇ»
10365 ccc
10366 bbb
10367 ccc"});
10368 cx.dispatch_action(SelectPrevious::default());
10369 cx.assert_editor_state(indoc! {"
10370 «bbbˇ»
10371 ccc
10372 «bbbˇ»
10373 ccc"});
10374}
10375
10376#[gpui::test]
10377async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
10378 init_test(cx, |_| {});
10379
10380 let mut cx = EditorTestContext::new(cx).await;
10381 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
10382
10383 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10384 .unwrap();
10385 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
10386
10387 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10388 .unwrap();
10389 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
10390
10391 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
10392 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
10393
10394 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
10395 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
10396
10397 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10398 .unwrap();
10399 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
10400
10401 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10402 .unwrap();
10403 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
10404}
10405
10406#[gpui::test]
10407async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
10408 init_test(cx, |_| {});
10409
10410 let mut cx = EditorTestContext::new(cx).await;
10411 cx.set_state("aˇ");
10412
10413 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10414 .unwrap();
10415 cx.assert_editor_state("«aˇ»");
10416 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10417 .unwrap();
10418 cx.assert_editor_state("«aˇ»");
10419}
10420
10421#[gpui::test]
10422async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
10423 init_test(cx, |_| {});
10424
10425 let mut cx = EditorTestContext::new(cx).await;
10426 cx.set_state(
10427 r#"let foo = 2;
10428lˇet foo = 2;
10429let fooˇ = 2;
10430let foo = 2;
10431let foo = ˇ2;"#,
10432 );
10433
10434 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10435 .unwrap();
10436 cx.assert_editor_state(
10437 r#"let foo = 2;
10438«letˇ» foo = 2;
10439let «fooˇ» = 2;
10440let foo = 2;
10441let foo = «2ˇ»;"#,
10442 );
10443
10444 // noop for multiple selections with different contents
10445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10446 .unwrap();
10447 cx.assert_editor_state(
10448 r#"let foo = 2;
10449«letˇ» foo = 2;
10450let «fooˇ» = 2;
10451let foo = 2;
10452let foo = «2ˇ»;"#,
10453 );
10454}
10455
10456#[gpui::test]
10457async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
10458 init_test(cx, |_| {});
10459 let mut cx = EditorTestContext::new(cx).await;
10460
10461 // Enable case sensitive search.
10462 update_test_editor_settings(&mut cx, &|settings| {
10463 let mut search_settings = SearchSettingsContent::default();
10464 search_settings.case_sensitive = Some(true);
10465 settings.search = Some(search_settings);
10466 });
10467
10468 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
10469
10470 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10471 .unwrap();
10472 // selection direction is preserved
10473 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
10474
10475 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10476 .unwrap();
10477 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
10478
10479 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
10480 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
10481
10482 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
10483 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
10484
10485 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10486 .unwrap();
10487 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
10488
10489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
10490 .unwrap();
10491 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
10492
10493 // Test case sensitivity
10494 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
10495 cx.update_editor(|e, window, cx| {
10496 e.select_previous(&SelectPrevious::default(), window, cx)
10497 .unwrap();
10498 e.select_previous(&SelectPrevious::default(), window, cx)
10499 .unwrap();
10500 });
10501 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
10502
10503 // Disable case sensitive search.
10504 update_test_editor_settings(&mut cx, &|settings| {
10505 let mut search_settings = SearchSettingsContent::default();
10506 search_settings.case_sensitive = Some(false);
10507 settings.search = Some(search_settings);
10508 });
10509
10510 cx.set_state("foo\nFOO\n«ˇFoo»");
10511 cx.update_editor(|e, window, cx| {
10512 e.select_previous(&SelectPrevious::default(), window, cx)
10513 .unwrap();
10514 e.select_previous(&SelectPrevious::default(), window, cx)
10515 .unwrap();
10516 });
10517 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
10518}
10519
10520#[gpui::test]
10521async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
10522 init_test(cx, |_| {});
10523
10524 let language = Arc::new(Language::new(
10525 LanguageConfig::default(),
10526 Some(tree_sitter_rust::LANGUAGE.into()),
10527 ));
10528
10529 let text = r#"
10530 use mod1::mod2::{mod3, mod4};
10531
10532 fn fn_1(param1: bool, param2: &str) {
10533 let var1 = "text";
10534 }
10535 "#
10536 .unindent();
10537
10538 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10539 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10540 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10541
10542 editor
10543 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10544 .await;
10545
10546 editor.update_in(cx, |editor, window, cx| {
10547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10548 s.select_display_ranges([
10549 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
10550 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
10551 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
10552 ]);
10553 });
10554 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10555 });
10556 editor.update(cx, |editor, cx| {
10557 assert_text_with_selections(
10558 editor,
10559 indoc! {r#"
10560 use mod1::mod2::{mod3, «mod4ˇ»};
10561
10562 fn fn_1«ˇ(param1: bool, param2: &str)» {
10563 let var1 = "«textˇ»";
10564 }
10565 "#},
10566 cx,
10567 );
10568 });
10569
10570 editor.update_in(cx, |editor, window, cx| {
10571 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10572 });
10573 editor.update(cx, |editor, cx| {
10574 assert_text_with_selections(
10575 editor,
10576 indoc! {r#"
10577 use mod1::mod2::«{mod3, mod4}ˇ»;
10578
10579 «ˇfn fn_1(param1: bool, param2: &str) {
10580 let var1 = "text";
10581 }»
10582 "#},
10583 cx,
10584 );
10585 });
10586
10587 editor.update_in(cx, |editor, window, cx| {
10588 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10589 });
10590 assert_eq!(
10591 editor.update(cx, |editor, cx| editor
10592 .selections
10593 .display_ranges(&editor.display_snapshot(cx))),
10594 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
10595 );
10596
10597 // Trying to expand the selected syntax node one more time has no effect.
10598 editor.update_in(cx, |editor, window, cx| {
10599 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10600 });
10601 assert_eq!(
10602 editor.update(cx, |editor, cx| editor
10603 .selections
10604 .display_ranges(&editor.display_snapshot(cx))),
10605 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
10606 );
10607
10608 editor.update_in(cx, |editor, window, cx| {
10609 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10610 });
10611 editor.update(cx, |editor, cx| {
10612 assert_text_with_selections(
10613 editor,
10614 indoc! {r#"
10615 use mod1::mod2::«{mod3, mod4}ˇ»;
10616
10617 «ˇfn fn_1(param1: bool, param2: &str) {
10618 let var1 = "text";
10619 }»
10620 "#},
10621 cx,
10622 );
10623 });
10624
10625 editor.update_in(cx, |editor, window, cx| {
10626 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10627 });
10628 editor.update(cx, |editor, cx| {
10629 assert_text_with_selections(
10630 editor,
10631 indoc! {r#"
10632 use mod1::mod2::{mod3, «mod4ˇ»};
10633
10634 fn fn_1«ˇ(param1: bool, param2: &str)» {
10635 let var1 = "«textˇ»";
10636 }
10637 "#},
10638 cx,
10639 );
10640 });
10641
10642 editor.update_in(cx, |editor, window, cx| {
10643 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10644 });
10645 editor.update(cx, |editor, cx| {
10646 assert_text_with_selections(
10647 editor,
10648 indoc! {r#"
10649 use mod1::mod2::{mod3, moˇd4};
10650
10651 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
10652 let var1 = "teˇxt";
10653 }
10654 "#},
10655 cx,
10656 );
10657 });
10658
10659 // Trying to shrink the selected syntax node one more time has no effect.
10660 editor.update_in(cx, |editor, window, cx| {
10661 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10662 });
10663 editor.update_in(cx, |editor, _, cx| {
10664 assert_text_with_selections(
10665 editor,
10666 indoc! {r#"
10667 use mod1::mod2::{mod3, moˇd4};
10668
10669 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
10670 let var1 = "teˇxt";
10671 }
10672 "#},
10673 cx,
10674 );
10675 });
10676
10677 // Ensure that we keep expanding the selection if the larger selection starts or ends within
10678 // a fold.
10679 editor.update_in(cx, |editor, window, cx| {
10680 editor.fold_creases(
10681 vec![
10682 Crease::simple(
10683 Point::new(0, 21)..Point::new(0, 24),
10684 FoldPlaceholder::test(),
10685 ),
10686 Crease::simple(
10687 Point::new(3, 20)..Point::new(3, 22),
10688 FoldPlaceholder::test(),
10689 ),
10690 ],
10691 true,
10692 window,
10693 cx,
10694 );
10695 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10696 });
10697 editor.update(cx, |editor, cx| {
10698 assert_text_with_selections(
10699 editor,
10700 indoc! {r#"
10701 use mod1::mod2::«{mod3, mod4}ˇ»;
10702
10703 fn fn_1«ˇ(param1: bool, param2: &str)» {
10704 let var1 = "«textˇ»";
10705 }
10706 "#},
10707 cx,
10708 );
10709 });
10710
10711 // Ensure multiple cursors have consistent direction after expanding
10712 editor.update_in(cx, |editor, window, cx| {
10713 editor.unfold_all(&UnfoldAll, window, cx);
10714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10715 s.select_display_ranges([
10716 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
10717 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
10718 ]);
10719 });
10720 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10721 });
10722 editor.update(cx, |editor, cx| {
10723 assert_text_with_selections(
10724 editor,
10725 indoc! {r#"
10726 use mod1::mod2::{mod3, «mod4ˇ»};
10727
10728 fn fn_1(param1: bool, param2: &str) {
10729 let var1 = "«textˇ»";
10730 }
10731 "#},
10732 cx,
10733 );
10734 });
10735}
10736
10737#[gpui::test]
10738async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
10739 init_test(cx, |_| {});
10740
10741 let language = Arc::new(Language::new(
10742 LanguageConfig::default(),
10743 Some(tree_sitter_rust::LANGUAGE.into()),
10744 ));
10745
10746 let text = "let a = 2;";
10747
10748 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10749 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10750 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10751
10752 editor
10753 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10754 .await;
10755
10756 // Test case 1: Cursor at end of word
10757 editor.update_in(cx, |editor, window, cx| {
10758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10759 s.select_display_ranges([
10760 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
10761 ]);
10762 });
10763 });
10764 editor.update(cx, |editor, cx| {
10765 assert_text_with_selections(editor, "let aˇ = 2;", cx);
10766 });
10767 editor.update_in(cx, |editor, window, cx| {
10768 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10769 });
10770 editor.update(cx, |editor, cx| {
10771 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
10772 });
10773 editor.update_in(cx, |editor, window, cx| {
10774 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10775 });
10776 editor.update(cx, |editor, cx| {
10777 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10778 });
10779
10780 // Test case 2: Cursor at end of statement
10781 editor.update_in(cx, |editor, window, cx| {
10782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10783 s.select_display_ranges([
10784 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
10785 ]);
10786 });
10787 });
10788 editor.update(cx, |editor, cx| {
10789 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
10790 });
10791 editor.update_in(cx, |editor, window, cx| {
10792 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10793 });
10794 editor.update(cx, |editor, cx| {
10795 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10796 });
10797}
10798
10799#[gpui::test]
10800async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
10801 init_test(cx, |_| {});
10802
10803 let language = Arc::new(Language::new(
10804 LanguageConfig {
10805 name: "JavaScript".into(),
10806 ..Default::default()
10807 },
10808 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10809 ));
10810
10811 let text = r#"
10812 let a = {
10813 key: "value",
10814 };
10815 "#
10816 .unindent();
10817
10818 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10819 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10820 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10821
10822 editor
10823 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10824 .await;
10825
10826 // Test case 1: Cursor after '{'
10827 editor.update_in(cx, |editor, window, cx| {
10828 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10829 s.select_display_ranges([
10830 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
10831 ]);
10832 });
10833 });
10834 editor.update(cx, |editor, cx| {
10835 assert_text_with_selections(
10836 editor,
10837 indoc! {r#"
10838 let a = {ˇ
10839 key: "value",
10840 };
10841 "#},
10842 cx,
10843 );
10844 });
10845 editor.update_in(cx, |editor, window, cx| {
10846 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10847 });
10848 editor.update(cx, |editor, cx| {
10849 assert_text_with_selections(
10850 editor,
10851 indoc! {r#"
10852 let a = «ˇ{
10853 key: "value",
10854 }»;
10855 "#},
10856 cx,
10857 );
10858 });
10859
10860 // Test case 2: Cursor after ':'
10861 editor.update_in(cx, |editor, window, cx| {
10862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10863 s.select_display_ranges([
10864 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
10865 ]);
10866 });
10867 });
10868 editor.update(cx, |editor, cx| {
10869 assert_text_with_selections(
10870 editor,
10871 indoc! {r#"
10872 let a = {
10873 key:ˇ "value",
10874 };
10875 "#},
10876 cx,
10877 );
10878 });
10879 editor.update_in(cx, |editor, window, cx| {
10880 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10881 });
10882 editor.update(cx, |editor, cx| {
10883 assert_text_with_selections(
10884 editor,
10885 indoc! {r#"
10886 let a = {
10887 «ˇkey: "value"»,
10888 };
10889 "#},
10890 cx,
10891 );
10892 });
10893 editor.update_in(cx, |editor, window, cx| {
10894 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10895 });
10896 editor.update(cx, |editor, cx| {
10897 assert_text_with_selections(
10898 editor,
10899 indoc! {r#"
10900 let a = «ˇ{
10901 key: "value",
10902 }»;
10903 "#},
10904 cx,
10905 );
10906 });
10907
10908 // Test case 3: Cursor after ','
10909 editor.update_in(cx, |editor, window, cx| {
10910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10911 s.select_display_ranges([
10912 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
10913 ]);
10914 });
10915 });
10916 editor.update(cx, |editor, cx| {
10917 assert_text_with_selections(
10918 editor,
10919 indoc! {r#"
10920 let a = {
10921 key: "value",ˇ
10922 };
10923 "#},
10924 cx,
10925 );
10926 });
10927 editor.update_in(cx, |editor, window, cx| {
10928 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10929 });
10930 editor.update(cx, |editor, cx| {
10931 assert_text_with_selections(
10932 editor,
10933 indoc! {r#"
10934 let a = «ˇ{
10935 key: "value",
10936 }»;
10937 "#},
10938 cx,
10939 );
10940 });
10941
10942 // Test case 4: Cursor after ';'
10943 editor.update_in(cx, |editor, window, cx| {
10944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10945 s.select_display_ranges([
10946 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
10947 ]);
10948 });
10949 });
10950 editor.update(cx, |editor, cx| {
10951 assert_text_with_selections(
10952 editor,
10953 indoc! {r#"
10954 let a = {
10955 key: "value",
10956 };ˇ
10957 "#},
10958 cx,
10959 );
10960 });
10961 editor.update_in(cx, |editor, window, cx| {
10962 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10963 });
10964 editor.update(cx, |editor, cx| {
10965 assert_text_with_selections(
10966 editor,
10967 indoc! {r#"
10968 «ˇlet a = {
10969 key: "value",
10970 };
10971 »"#},
10972 cx,
10973 );
10974 });
10975}
10976
10977#[gpui::test]
10978async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
10979 init_test(cx, |_| {});
10980
10981 let language = Arc::new(Language::new(
10982 LanguageConfig::default(),
10983 Some(tree_sitter_rust::LANGUAGE.into()),
10984 ));
10985
10986 let text = r#"
10987 use mod1::mod2::{mod3, mod4};
10988
10989 fn fn_1(param1: bool, param2: &str) {
10990 let var1 = "hello world";
10991 }
10992 "#
10993 .unindent();
10994
10995 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10996 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10997 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10998
10999 editor
11000 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11001 .await;
11002
11003 // Test 1: Cursor on a letter of a string word
11004 editor.update_in(cx, |editor, window, cx| {
11005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11006 s.select_display_ranges([
11007 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
11008 ]);
11009 });
11010 });
11011 editor.update_in(cx, |editor, window, cx| {
11012 assert_text_with_selections(
11013 editor,
11014 indoc! {r#"
11015 use mod1::mod2::{mod3, mod4};
11016
11017 fn fn_1(param1: bool, param2: &str) {
11018 let var1 = "hˇello world";
11019 }
11020 "#},
11021 cx,
11022 );
11023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11024 assert_text_with_selections(
11025 editor,
11026 indoc! {r#"
11027 use mod1::mod2::{mod3, mod4};
11028
11029 fn fn_1(param1: bool, param2: &str) {
11030 let var1 = "«ˇhello» world";
11031 }
11032 "#},
11033 cx,
11034 );
11035 });
11036
11037 // Test 2: Partial selection within a word
11038 editor.update_in(cx, |editor, window, cx| {
11039 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11040 s.select_display_ranges([
11041 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
11042 ]);
11043 });
11044 });
11045 editor.update_in(cx, |editor, window, cx| {
11046 assert_text_with_selections(
11047 editor,
11048 indoc! {r#"
11049 use mod1::mod2::{mod3, mod4};
11050
11051 fn fn_1(param1: bool, param2: &str) {
11052 let var1 = "h«elˇ»lo world";
11053 }
11054 "#},
11055 cx,
11056 );
11057 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11058 assert_text_with_selections(
11059 editor,
11060 indoc! {r#"
11061 use mod1::mod2::{mod3, mod4};
11062
11063 fn fn_1(param1: bool, param2: &str) {
11064 let var1 = "«ˇhello» world";
11065 }
11066 "#},
11067 cx,
11068 );
11069 });
11070
11071 // Test 3: Complete word already selected
11072 editor.update_in(cx, |editor, window, cx| {
11073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11074 s.select_display_ranges([
11075 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
11076 ]);
11077 });
11078 });
11079 editor.update_in(cx, |editor, window, cx| {
11080 assert_text_with_selections(
11081 editor,
11082 indoc! {r#"
11083 use mod1::mod2::{mod3, mod4};
11084
11085 fn fn_1(param1: bool, param2: &str) {
11086 let var1 = "«helloˇ» world";
11087 }
11088 "#},
11089 cx,
11090 );
11091 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11092 assert_text_with_selections(
11093 editor,
11094 indoc! {r#"
11095 use mod1::mod2::{mod3, mod4};
11096
11097 fn fn_1(param1: bool, param2: &str) {
11098 let var1 = "«hello worldˇ»";
11099 }
11100 "#},
11101 cx,
11102 );
11103 });
11104
11105 // Test 4: Selection spanning across words
11106 editor.update_in(cx, |editor, window, cx| {
11107 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11108 s.select_display_ranges([
11109 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
11110 ]);
11111 });
11112 });
11113 editor.update_in(cx, |editor, window, cx| {
11114 assert_text_with_selections(
11115 editor,
11116 indoc! {r#"
11117 use mod1::mod2::{mod3, mod4};
11118
11119 fn fn_1(param1: bool, param2: &str) {
11120 let var1 = "hel«lo woˇ»rld";
11121 }
11122 "#},
11123 cx,
11124 );
11125 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11126 assert_text_with_selections(
11127 editor,
11128 indoc! {r#"
11129 use mod1::mod2::{mod3, mod4};
11130
11131 fn fn_1(param1: bool, param2: &str) {
11132 let var1 = "«ˇhello world»";
11133 }
11134 "#},
11135 cx,
11136 );
11137 });
11138
11139 // Test 5: Expansion beyond string
11140 editor.update_in(cx, |editor, window, cx| {
11141 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11142 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
11143 assert_text_with_selections(
11144 editor,
11145 indoc! {r#"
11146 use mod1::mod2::{mod3, mod4};
11147
11148 fn fn_1(param1: bool, param2: &str) {
11149 «ˇlet var1 = "hello world";»
11150 }
11151 "#},
11152 cx,
11153 );
11154 });
11155}
11156
11157#[gpui::test]
11158async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
11159 init_test(cx, |_| {});
11160
11161 let mut cx = EditorTestContext::new(cx).await;
11162
11163 let language = Arc::new(Language::new(
11164 LanguageConfig::default(),
11165 Some(tree_sitter_rust::LANGUAGE.into()),
11166 ));
11167
11168 cx.update_buffer(|buffer, cx| {
11169 buffer.set_language(Some(language), cx);
11170 });
11171
11172 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
11173 cx.update_editor(|editor, window, cx| {
11174 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
11175 });
11176
11177 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
11178
11179 cx.set_state(indoc! { r#"fn a() {
11180 // what
11181 // a
11182 // ˇlong
11183 // method
11184 // I
11185 // sure
11186 // hope
11187 // it
11188 // works
11189 }"# });
11190
11191 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
11192 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
11193 cx.update(|_, cx| {
11194 multi_buffer.update(cx, |multi_buffer, cx| {
11195 multi_buffer.set_excerpts_for_path(
11196 PathKey::for_buffer(&buffer, cx),
11197 buffer,
11198 [Point::new(1, 0)..Point::new(1, 0)],
11199 3,
11200 cx,
11201 );
11202 });
11203 });
11204
11205 let editor2 = cx.new_window_entity(|window, cx| {
11206 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
11207 });
11208
11209 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
11210 cx.update_editor(|editor, window, cx| {
11211 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11212 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
11213 })
11214 });
11215
11216 cx.assert_editor_state(indoc! { "
11217 fn a() {
11218 // what
11219 // a
11220 ˇ // long
11221 // method"});
11222
11223 cx.update_editor(|editor, window, cx| {
11224 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
11225 });
11226
11227 // Although we could potentially make the action work when the syntax node
11228 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
11229 // did. Maybe we could also expand the excerpt to contain the range?
11230 cx.assert_editor_state(indoc! { "
11231 fn a() {
11232 // what
11233 // a
11234 ˇ // long
11235 // method"});
11236}
11237
11238#[gpui::test]
11239async fn test_fold_function_bodies(cx: &mut TestAppContext) {
11240 init_test(cx, |_| {});
11241
11242 let base_text = r#"
11243 impl A {
11244 // this is an uncommitted comment
11245
11246 fn b() {
11247 c();
11248 }
11249
11250 // this is another uncommitted comment
11251
11252 fn d() {
11253 // e
11254 // f
11255 }
11256 }
11257
11258 fn g() {
11259 // h
11260 }
11261 "#
11262 .unindent();
11263
11264 let text = r#"
11265 ˇimpl A {
11266
11267 fn b() {
11268 c();
11269 }
11270
11271 fn d() {
11272 // e
11273 // f
11274 }
11275 }
11276
11277 fn g() {
11278 // h
11279 }
11280 "#
11281 .unindent();
11282
11283 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
11284 cx.set_state(&text);
11285 cx.set_head_text(&base_text);
11286 cx.update_editor(|editor, window, cx| {
11287 editor.expand_all_diff_hunks(&Default::default(), window, cx);
11288 });
11289
11290 cx.assert_state_with_diff(
11291 "
11292 ˇimpl A {
11293 - // this is an uncommitted comment
11294
11295 fn b() {
11296 c();
11297 }
11298
11299 - // this is another uncommitted comment
11300 -
11301 fn d() {
11302 // e
11303 // f
11304 }
11305 }
11306
11307 fn g() {
11308 // h
11309 }
11310 "
11311 .unindent(),
11312 );
11313
11314 let expected_display_text = "
11315 impl A {
11316 // this is an uncommitted comment
11317
11318 fn b() {
11319 ⋯
11320 }
11321
11322 // this is another uncommitted comment
11323
11324 fn d() {
11325 ⋯
11326 }
11327 }
11328
11329 fn g() {
11330 ⋯
11331 }
11332 "
11333 .unindent();
11334
11335 cx.update_editor(|editor, window, cx| {
11336 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
11337 assert_eq!(editor.display_text(cx), expected_display_text);
11338 });
11339}
11340
11341#[gpui::test]
11342async fn test_autoindent(cx: &mut TestAppContext) {
11343 init_test(cx, |_| {});
11344
11345 let language = Arc::new(
11346 Language::new(
11347 LanguageConfig {
11348 brackets: BracketPairConfig {
11349 pairs: vec![
11350 BracketPair {
11351 start: "{".to_string(),
11352 end: "}".to_string(),
11353 close: false,
11354 surround: false,
11355 newline: true,
11356 },
11357 BracketPair {
11358 start: "(".to_string(),
11359 end: ")".to_string(),
11360 close: false,
11361 surround: false,
11362 newline: true,
11363 },
11364 ],
11365 ..Default::default()
11366 },
11367 ..Default::default()
11368 },
11369 Some(tree_sitter_rust::LANGUAGE.into()),
11370 )
11371 .with_indents_query(
11372 r#"
11373 (_ "(" ")" @end) @indent
11374 (_ "{" "}" @end) @indent
11375 "#,
11376 )
11377 .unwrap(),
11378 );
11379
11380 let text = "fn a() {}";
11381
11382 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11383 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11384 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11385 editor
11386 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11387 .await;
11388
11389 editor.update_in(cx, |editor, window, cx| {
11390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11391 s.select_ranges([
11392 MultiBufferOffset(5)..MultiBufferOffset(5),
11393 MultiBufferOffset(8)..MultiBufferOffset(8),
11394 MultiBufferOffset(9)..MultiBufferOffset(9),
11395 ])
11396 });
11397 editor.newline(&Newline, window, cx);
11398 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
11399 assert_eq!(
11400 editor.selections.ranges(&editor.display_snapshot(cx)),
11401 &[
11402 Point::new(1, 4)..Point::new(1, 4),
11403 Point::new(3, 4)..Point::new(3, 4),
11404 Point::new(5, 0)..Point::new(5, 0)
11405 ]
11406 );
11407 });
11408}
11409
11410#[gpui::test]
11411async fn test_autoindent_disabled(cx: &mut TestAppContext) {
11412 init_test(cx, |settings| {
11413 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
11414 });
11415
11416 let language = Arc::new(
11417 Language::new(
11418 LanguageConfig {
11419 brackets: BracketPairConfig {
11420 pairs: vec![
11421 BracketPair {
11422 start: "{".to_string(),
11423 end: "}".to_string(),
11424 close: false,
11425 surround: false,
11426 newline: true,
11427 },
11428 BracketPair {
11429 start: "(".to_string(),
11430 end: ")".to_string(),
11431 close: false,
11432 surround: false,
11433 newline: true,
11434 },
11435 ],
11436 ..Default::default()
11437 },
11438 ..Default::default()
11439 },
11440 Some(tree_sitter_rust::LANGUAGE.into()),
11441 )
11442 .with_indents_query(
11443 r#"
11444 (_ "(" ")" @end) @indent
11445 (_ "{" "}" @end) @indent
11446 "#,
11447 )
11448 .unwrap(),
11449 );
11450
11451 let text = "fn a() {}";
11452
11453 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11454 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11455 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11456 editor
11457 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11458 .await;
11459
11460 editor.update_in(cx, |editor, window, cx| {
11461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11462 s.select_ranges([
11463 MultiBufferOffset(5)..MultiBufferOffset(5),
11464 MultiBufferOffset(8)..MultiBufferOffset(8),
11465 MultiBufferOffset(9)..MultiBufferOffset(9),
11466 ])
11467 });
11468 editor.newline(&Newline, window, cx);
11469 assert_eq!(
11470 editor.text(cx),
11471 indoc!(
11472 "
11473 fn a(
11474
11475 ) {
11476
11477 }
11478 "
11479 )
11480 );
11481 assert_eq!(
11482 editor.selections.ranges(&editor.display_snapshot(cx)),
11483 &[
11484 Point::new(1, 0)..Point::new(1, 0),
11485 Point::new(3, 0)..Point::new(3, 0),
11486 Point::new(5, 0)..Point::new(5, 0)
11487 ]
11488 );
11489 });
11490}
11491
11492#[gpui::test]
11493async fn test_autoindent_none_does_not_preserve_indentation_on_newline(cx: &mut TestAppContext) {
11494 init_test(cx, |settings| {
11495 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
11496 });
11497
11498 let mut cx = EditorTestContext::new(cx).await;
11499
11500 cx.set_state(indoc! {"
11501 hello
11502 indented lineˇ
11503 world
11504 "});
11505
11506 cx.update_editor(|editor, window, cx| {
11507 editor.newline(&Newline, window, cx);
11508 });
11509
11510 cx.assert_editor_state(indoc! {"
11511 hello
11512 indented line
11513 ˇ
11514 world
11515 "});
11516}
11517
11518#[gpui::test]
11519async fn test_autoindent_preserve_indent_maintains_indentation_on_newline(cx: &mut TestAppContext) {
11520 // When auto_indent is "preserve_indent", pressing Enter on an indented line
11521 // should preserve the indentation but not adjust based on syntax.
11522 init_test(cx, |settings| {
11523 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
11524 });
11525
11526 let mut cx = EditorTestContext::new(cx).await;
11527
11528 cx.set_state(indoc! {"
11529 hello
11530 indented lineˇ
11531 world
11532 "});
11533
11534 cx.update_editor(|editor, window, cx| {
11535 editor.newline(&Newline, window, cx);
11536 });
11537
11538 // The new line SHOULD have the same indentation as the previous line
11539 cx.assert_editor_state(indoc! {"
11540 hello
11541 indented line
11542 ˇ
11543 world
11544 "});
11545}
11546
11547#[gpui::test]
11548async fn test_autoindent_preserve_indent_does_not_apply_syntax_indent(cx: &mut TestAppContext) {
11549 init_test(cx, |settings| {
11550 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
11551 });
11552
11553 let language = Arc::new(
11554 Language::new(
11555 LanguageConfig {
11556 brackets: BracketPairConfig {
11557 pairs: vec![BracketPair {
11558 start: "{".to_string(),
11559 end: "}".to_string(),
11560 close: false,
11561 surround: false,
11562 newline: false, // Disable extra newline behavior to isolate syntax indent test
11563 }],
11564 ..Default::default()
11565 },
11566 ..Default::default()
11567 },
11568 Some(tree_sitter_rust::LANGUAGE.into()),
11569 )
11570 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11571 .unwrap(),
11572 );
11573
11574 let buffer =
11575 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11578 editor
11579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11580 .await;
11581
11582 // Position cursor at end of line containing `{`
11583 editor.update_in(cx, |editor, window, cx| {
11584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11585 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11586 });
11587 editor.newline(&Newline, window, cx);
11588
11589 // With PreserveIndent, the new line should have 0 indentation (same as the fn line)
11590 // NOT 4 spaces (which tree-sitter would add for being inside `{}`)
11591 assert_eq!(editor.text(cx), "fn foo() {\n\n}");
11592 });
11593}
11594
11595#[gpui::test]
11596async fn test_autoindent_syntax_aware_applies_syntax_indent(cx: &mut TestAppContext) {
11597 // Companion test to show that SyntaxAware DOES apply tree-sitter indentation
11598 init_test(cx, |settings| {
11599 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware)
11600 });
11601
11602 let language = Arc::new(
11603 Language::new(
11604 LanguageConfig {
11605 brackets: BracketPairConfig {
11606 pairs: vec![BracketPair {
11607 start: "{".to_string(),
11608 end: "}".to_string(),
11609 close: false,
11610 surround: false,
11611 newline: false, // Disable extra newline behavior to isolate syntax indent test
11612 }],
11613 ..Default::default()
11614 },
11615 ..Default::default()
11616 },
11617 Some(tree_sitter_rust::LANGUAGE.into()),
11618 )
11619 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11620 .unwrap(),
11621 );
11622
11623 let buffer =
11624 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11625 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11626 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11627 editor
11628 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11629 .await;
11630
11631 // Position cursor at end of line containing `{`
11632 editor.update_in(cx, |editor, window, cx| {
11633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11634 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11635 });
11636 editor.newline(&Newline, window, cx);
11637
11638 // With SyntaxAware, tree-sitter adds indentation for being inside `{}`
11639 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
11640 });
11641}
11642
11643#[gpui::test]
11644async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
11645 init_test(cx, |settings| {
11646 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware);
11647 settings.languages.0.insert(
11648 "python".into(),
11649 LanguageSettingsContent {
11650 auto_indent: Some(settings::AutoIndentMode::None),
11651 ..Default::default()
11652 },
11653 );
11654 });
11655
11656 let mut cx = EditorTestContext::new(cx).await;
11657
11658 let injected_language = Arc::new(
11659 Language::new(
11660 LanguageConfig {
11661 brackets: BracketPairConfig {
11662 pairs: vec![
11663 BracketPair {
11664 start: "{".to_string(),
11665 end: "}".to_string(),
11666 close: false,
11667 surround: false,
11668 newline: true,
11669 },
11670 BracketPair {
11671 start: "(".to_string(),
11672 end: ")".to_string(),
11673 close: true,
11674 surround: false,
11675 newline: true,
11676 },
11677 ],
11678 ..Default::default()
11679 },
11680 name: "python".into(),
11681 ..Default::default()
11682 },
11683 Some(tree_sitter_python::LANGUAGE.into()),
11684 )
11685 .with_indents_query(
11686 r#"
11687 (_ "(" ")" @end) @indent
11688 (_ "{" "}" @end) @indent
11689 "#,
11690 )
11691 .unwrap(),
11692 );
11693
11694 let language = Arc::new(
11695 Language::new(
11696 LanguageConfig {
11697 brackets: BracketPairConfig {
11698 pairs: vec![
11699 BracketPair {
11700 start: "{".to_string(),
11701 end: "}".to_string(),
11702 close: false,
11703 surround: false,
11704 newline: true,
11705 },
11706 BracketPair {
11707 start: "(".to_string(),
11708 end: ")".to_string(),
11709 close: true,
11710 surround: false,
11711 newline: true,
11712 },
11713 ],
11714 ..Default::default()
11715 },
11716 name: LanguageName::new_static("rust"),
11717 ..Default::default()
11718 },
11719 Some(tree_sitter_rust::LANGUAGE.into()),
11720 )
11721 .with_indents_query(
11722 r#"
11723 (_ "(" ")" @end) @indent
11724 (_ "{" "}" @end) @indent
11725 "#,
11726 )
11727 .unwrap()
11728 .with_injection_query(
11729 r#"
11730 (macro_invocation
11731 macro: (identifier) @_macro_name
11732 (token_tree) @injection.content
11733 (#set! injection.language "python"))
11734 "#,
11735 )
11736 .unwrap(),
11737 );
11738
11739 cx.language_registry().add(injected_language);
11740 cx.language_registry().add(language.clone());
11741
11742 cx.update_buffer(|buffer, cx| {
11743 buffer.set_language(Some(language), cx);
11744 });
11745
11746 cx.set_state(r#"struct A {ˇ}"#);
11747
11748 cx.update_editor(|editor, window, cx| {
11749 editor.newline(&Default::default(), window, cx);
11750 });
11751
11752 cx.assert_editor_state(indoc!(
11753 "struct A {
11754 ˇ
11755 }"
11756 ));
11757
11758 cx.set_state(r#"select_biased!(ˇ)"#);
11759
11760 cx.update_editor(|editor, window, cx| {
11761 editor.newline(&Default::default(), window, cx);
11762 editor.handle_input("def ", window, cx);
11763 editor.handle_input("(", window, cx);
11764 editor.newline(&Default::default(), window, cx);
11765 editor.handle_input("a", window, cx);
11766 });
11767
11768 cx.assert_editor_state(indoc!(
11769 "select_biased!(
11770 def (
11771 aˇ
11772 )
11773 )"
11774 ));
11775}
11776
11777#[gpui::test]
11778async fn test_autoindent_selections(cx: &mut TestAppContext) {
11779 init_test(cx, |_| {});
11780
11781 {
11782 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
11783 cx.set_state(indoc! {"
11784 impl A {
11785
11786 fn b() {}
11787
11788 «fn c() {
11789
11790 }ˇ»
11791 }
11792 "});
11793
11794 cx.update_editor(|editor, window, cx| {
11795 editor.autoindent(&Default::default(), window, cx);
11796 });
11797 cx.wait_for_autoindent_applied().await;
11798
11799 cx.assert_editor_state(indoc! {"
11800 impl A {
11801
11802 fn b() {}
11803
11804 «fn c() {
11805
11806 }ˇ»
11807 }
11808 "});
11809 }
11810
11811 {
11812 let mut cx = EditorTestContext::new_multibuffer(
11813 cx,
11814 [indoc! { "
11815 impl A {
11816 «
11817 // a
11818 fn b(){}
11819 »
11820 «
11821 }
11822 fn c(){}
11823 »
11824 "}],
11825 );
11826
11827 let buffer = cx.update_editor(|editor, _, cx| {
11828 let buffer = editor.buffer().update(cx, |buffer, _| {
11829 buffer.all_buffers().iter().next().unwrap().clone()
11830 });
11831 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
11832 buffer
11833 });
11834
11835 cx.run_until_parked();
11836 cx.update_editor(|editor, window, cx| {
11837 editor.select_all(&Default::default(), window, cx);
11838 editor.autoindent(&Default::default(), window, cx)
11839 });
11840 cx.run_until_parked();
11841
11842 cx.update(|_, cx| {
11843 assert_eq!(
11844 buffer.read(cx).text(),
11845 indoc! { "
11846 impl A {
11847
11848 // a
11849 fn b(){}
11850
11851
11852 }
11853 fn c(){}
11854
11855 " }
11856 )
11857 });
11858 }
11859}
11860
11861#[gpui::test]
11862async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
11863 init_test(cx, |_| {});
11864
11865 let mut cx = EditorTestContext::new(cx).await;
11866
11867 let language = Arc::new(Language::new(
11868 LanguageConfig {
11869 brackets: BracketPairConfig {
11870 pairs: vec![
11871 BracketPair {
11872 start: "{".to_string(),
11873 end: "}".to_string(),
11874 close: true,
11875 surround: true,
11876 newline: true,
11877 },
11878 BracketPair {
11879 start: "(".to_string(),
11880 end: ")".to_string(),
11881 close: true,
11882 surround: true,
11883 newline: true,
11884 },
11885 BracketPair {
11886 start: "/*".to_string(),
11887 end: " */".to_string(),
11888 close: true,
11889 surround: true,
11890 newline: true,
11891 },
11892 BracketPair {
11893 start: "[".to_string(),
11894 end: "]".to_string(),
11895 close: false,
11896 surround: false,
11897 newline: true,
11898 },
11899 BracketPair {
11900 start: "\"".to_string(),
11901 end: "\"".to_string(),
11902 close: true,
11903 surround: true,
11904 newline: false,
11905 },
11906 BracketPair {
11907 start: "<".to_string(),
11908 end: ">".to_string(),
11909 close: false,
11910 surround: true,
11911 newline: true,
11912 },
11913 ],
11914 ..Default::default()
11915 },
11916 autoclose_before: "})]".to_string(),
11917 ..Default::default()
11918 },
11919 Some(tree_sitter_rust::LANGUAGE.into()),
11920 ));
11921
11922 cx.language_registry().add(language.clone());
11923 cx.update_buffer(|buffer, cx| {
11924 buffer.set_language(Some(language), cx);
11925 });
11926
11927 cx.set_state(
11928 &r#"
11929 🏀ˇ
11930 εˇ
11931 ❤️ˇ
11932 "#
11933 .unindent(),
11934 );
11935
11936 // autoclose multiple nested brackets at multiple cursors
11937 cx.update_editor(|editor, window, cx| {
11938 editor.handle_input("{", window, cx);
11939 editor.handle_input("{", window, cx);
11940 editor.handle_input("{", window, cx);
11941 });
11942 cx.assert_editor_state(
11943 &"
11944 🏀{{{ˇ}}}
11945 ε{{{ˇ}}}
11946 ❤️{{{ˇ}}}
11947 "
11948 .unindent(),
11949 );
11950
11951 // insert a different closing bracket
11952 cx.update_editor(|editor, window, cx| {
11953 editor.handle_input(")", window, cx);
11954 });
11955 cx.assert_editor_state(
11956 &"
11957 🏀{{{)ˇ}}}
11958 ε{{{)ˇ}}}
11959 ❤️{{{)ˇ}}}
11960 "
11961 .unindent(),
11962 );
11963
11964 // skip over the auto-closed brackets when typing a closing bracket
11965 cx.update_editor(|editor, window, cx| {
11966 editor.move_right(&MoveRight, window, cx);
11967 editor.handle_input("}", window, cx);
11968 editor.handle_input("}", window, cx);
11969 editor.handle_input("}", window, cx);
11970 });
11971 cx.assert_editor_state(
11972 &"
11973 🏀{{{)}}}}ˇ
11974 ε{{{)}}}}ˇ
11975 ❤️{{{)}}}}ˇ
11976 "
11977 .unindent(),
11978 );
11979
11980 // autoclose multi-character pairs
11981 cx.set_state(
11982 &"
11983 ˇ
11984 ˇ
11985 "
11986 .unindent(),
11987 );
11988 cx.update_editor(|editor, window, cx| {
11989 editor.handle_input("/", window, cx);
11990 editor.handle_input("*", window, cx);
11991 });
11992 cx.assert_editor_state(
11993 &"
11994 /*ˇ */
11995 /*ˇ */
11996 "
11997 .unindent(),
11998 );
11999
12000 // one cursor autocloses a multi-character pair, one cursor
12001 // does not autoclose.
12002 cx.set_state(
12003 &"
12004 /ˇ
12005 ˇ
12006 "
12007 .unindent(),
12008 );
12009 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
12010 cx.assert_editor_state(
12011 &"
12012 /*ˇ */
12013 *ˇ
12014 "
12015 .unindent(),
12016 );
12017
12018 // Don't autoclose if the next character isn't whitespace and isn't
12019 // listed in the language's "autoclose_before" section.
12020 cx.set_state("ˇa b");
12021 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
12022 cx.assert_editor_state("{ˇa b");
12023
12024 // Don't autoclose if `close` is false for the bracket pair
12025 cx.set_state("ˇ");
12026 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
12027 cx.assert_editor_state("[ˇ");
12028
12029 // Surround with brackets if text is selected
12030 cx.set_state("«aˇ» b");
12031 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
12032 cx.assert_editor_state("{«aˇ»} b");
12033
12034 // Autoclose when not immediately after a word character
12035 cx.set_state("a ˇ");
12036 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
12037 cx.assert_editor_state("a \"ˇ\"");
12038
12039 // Autoclose pair where the start and end characters are the same
12040 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
12041 cx.assert_editor_state("a \"\"ˇ");
12042
12043 // Don't autoclose when immediately after a word character
12044 cx.set_state("aˇ");
12045 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
12046 cx.assert_editor_state("a\"ˇ");
12047
12048 // Do autoclose when after a non-word character
12049 cx.set_state("{ˇ");
12050 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
12051 cx.assert_editor_state("{\"ˇ\"");
12052
12053 // Non identical pairs autoclose regardless of preceding character
12054 cx.set_state("aˇ");
12055 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
12056 cx.assert_editor_state("a{ˇ}");
12057
12058 // Don't autoclose pair if autoclose is disabled
12059 cx.set_state("ˇ");
12060 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
12061 cx.assert_editor_state("<ˇ");
12062
12063 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
12064 cx.set_state("«aˇ» b");
12065 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
12066 cx.assert_editor_state("<«aˇ»> b");
12067}
12068
12069#[gpui::test]
12070async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
12071 init_test(cx, |settings| {
12072 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
12073 });
12074
12075 let mut cx = EditorTestContext::new(cx).await;
12076
12077 let language = Arc::new(Language::new(
12078 LanguageConfig {
12079 brackets: BracketPairConfig {
12080 pairs: vec![
12081 BracketPair {
12082 start: "{".to_string(),
12083 end: "}".to_string(),
12084 close: true,
12085 surround: true,
12086 newline: true,
12087 },
12088 BracketPair {
12089 start: "(".to_string(),
12090 end: ")".to_string(),
12091 close: true,
12092 surround: true,
12093 newline: true,
12094 },
12095 BracketPair {
12096 start: "[".to_string(),
12097 end: "]".to_string(),
12098 close: false,
12099 surround: false,
12100 newline: true,
12101 },
12102 ],
12103 ..Default::default()
12104 },
12105 autoclose_before: "})]".to_string(),
12106 ..Default::default()
12107 },
12108 Some(tree_sitter_rust::LANGUAGE.into()),
12109 ));
12110
12111 cx.language_registry().add(language.clone());
12112 cx.update_buffer(|buffer, cx| {
12113 buffer.set_language(Some(language), cx);
12114 });
12115
12116 cx.set_state(
12117 &"
12118 ˇ
12119 ˇ
12120 ˇ
12121 "
12122 .unindent(),
12123 );
12124
12125 // ensure only matching closing brackets are skipped over
12126 cx.update_editor(|editor, window, cx| {
12127 editor.handle_input("}", window, cx);
12128 editor.move_left(&MoveLeft, window, cx);
12129 editor.handle_input(")", window, cx);
12130 editor.move_left(&MoveLeft, window, cx);
12131 });
12132 cx.assert_editor_state(
12133 &"
12134 ˇ)}
12135 ˇ)}
12136 ˇ)}
12137 "
12138 .unindent(),
12139 );
12140
12141 // skip-over closing brackets at multiple cursors
12142 cx.update_editor(|editor, window, cx| {
12143 editor.handle_input(")", window, cx);
12144 editor.handle_input("}", window, cx);
12145 });
12146 cx.assert_editor_state(
12147 &"
12148 )}ˇ
12149 )}ˇ
12150 )}ˇ
12151 "
12152 .unindent(),
12153 );
12154
12155 // ignore non-close brackets
12156 cx.update_editor(|editor, window, cx| {
12157 editor.handle_input("]", window, cx);
12158 editor.move_left(&MoveLeft, window, cx);
12159 editor.handle_input("]", window, cx);
12160 });
12161 cx.assert_editor_state(
12162 &"
12163 )}]ˇ]
12164 )}]ˇ]
12165 )}]ˇ]
12166 "
12167 .unindent(),
12168 );
12169}
12170
12171#[gpui::test]
12172async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
12173 init_test(cx, |_| {});
12174
12175 let mut cx = EditorTestContext::new(cx).await;
12176
12177 let html_language = Arc::new(
12178 Language::new(
12179 LanguageConfig {
12180 name: "HTML".into(),
12181 brackets: BracketPairConfig {
12182 pairs: vec![
12183 BracketPair {
12184 start: "<".into(),
12185 end: ">".into(),
12186 close: true,
12187 ..Default::default()
12188 },
12189 BracketPair {
12190 start: "{".into(),
12191 end: "}".into(),
12192 close: true,
12193 ..Default::default()
12194 },
12195 BracketPair {
12196 start: "(".into(),
12197 end: ")".into(),
12198 close: true,
12199 ..Default::default()
12200 },
12201 ],
12202 ..Default::default()
12203 },
12204 autoclose_before: "})]>".into(),
12205 ..Default::default()
12206 },
12207 Some(tree_sitter_html::LANGUAGE.into()),
12208 )
12209 .with_injection_query(
12210 r#"
12211 (script_element
12212 (raw_text) @injection.content
12213 (#set! injection.language "javascript"))
12214 "#,
12215 )
12216 .unwrap(),
12217 );
12218
12219 let javascript_language = Arc::new(Language::new(
12220 LanguageConfig {
12221 name: "JavaScript".into(),
12222 brackets: BracketPairConfig {
12223 pairs: vec![
12224 BracketPair {
12225 start: "/*".into(),
12226 end: " */".into(),
12227 close: true,
12228 ..Default::default()
12229 },
12230 BracketPair {
12231 start: "{".into(),
12232 end: "}".into(),
12233 close: true,
12234 ..Default::default()
12235 },
12236 BracketPair {
12237 start: "(".into(),
12238 end: ")".into(),
12239 close: true,
12240 ..Default::default()
12241 },
12242 ],
12243 ..Default::default()
12244 },
12245 autoclose_before: "})]>".into(),
12246 ..Default::default()
12247 },
12248 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12249 ));
12250
12251 cx.language_registry().add(html_language.clone());
12252 cx.language_registry().add(javascript_language);
12253 cx.executor().run_until_parked();
12254
12255 cx.update_buffer(|buffer, cx| {
12256 buffer.set_language(Some(html_language), cx);
12257 });
12258
12259 cx.set_state(
12260 &r#"
12261 <body>ˇ
12262 <script>
12263 var x = 1;ˇ
12264 </script>
12265 </body>ˇ
12266 "#
12267 .unindent(),
12268 );
12269
12270 // Precondition: different languages are active at different locations.
12271 cx.update_editor(|editor, window, cx| {
12272 let snapshot = editor.snapshot(window, cx);
12273 let cursors = editor
12274 .selections
12275 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
12276 let languages = cursors
12277 .iter()
12278 .map(|c| snapshot.language_at(c.start).unwrap().name())
12279 .collect::<Vec<_>>();
12280 assert_eq!(
12281 languages,
12282 &[
12283 LanguageName::from("HTML"),
12284 LanguageName::from("JavaScript"),
12285 LanguageName::from("HTML"),
12286 ]
12287 );
12288 });
12289
12290 // Angle brackets autoclose in HTML, but not JavaScript.
12291 cx.update_editor(|editor, window, cx| {
12292 editor.handle_input("<", window, cx);
12293 editor.handle_input("a", window, cx);
12294 });
12295 cx.assert_editor_state(
12296 &r#"
12297 <body><aˇ>
12298 <script>
12299 var x = 1;<aˇ
12300 </script>
12301 </body><aˇ>
12302 "#
12303 .unindent(),
12304 );
12305
12306 // Curly braces and parens autoclose in both HTML and JavaScript.
12307 cx.update_editor(|editor, window, cx| {
12308 editor.handle_input(" b=", window, cx);
12309 editor.handle_input("{", window, cx);
12310 editor.handle_input("c", window, cx);
12311 editor.handle_input("(", window, cx);
12312 });
12313 cx.assert_editor_state(
12314 &r#"
12315 <body><a b={c(ˇ)}>
12316 <script>
12317 var x = 1;<a b={c(ˇ)}
12318 </script>
12319 </body><a b={c(ˇ)}>
12320 "#
12321 .unindent(),
12322 );
12323
12324 // Brackets that were already autoclosed are skipped.
12325 cx.update_editor(|editor, window, cx| {
12326 editor.handle_input(")", window, cx);
12327 editor.handle_input("d", window, cx);
12328 editor.handle_input("}", window, cx);
12329 });
12330 cx.assert_editor_state(
12331 &r#"
12332 <body><a b={c()d}ˇ>
12333 <script>
12334 var x = 1;<a b={c()d}ˇ
12335 </script>
12336 </body><a b={c()d}ˇ>
12337 "#
12338 .unindent(),
12339 );
12340 cx.update_editor(|editor, window, cx| {
12341 editor.handle_input(">", window, cx);
12342 });
12343 cx.assert_editor_state(
12344 &r#"
12345 <body><a b={c()d}>ˇ
12346 <script>
12347 var x = 1;<a b={c()d}>ˇ
12348 </script>
12349 </body><a b={c()d}>ˇ
12350 "#
12351 .unindent(),
12352 );
12353
12354 // Reset
12355 cx.set_state(
12356 &r#"
12357 <body>ˇ
12358 <script>
12359 var x = 1;ˇ
12360 </script>
12361 </body>ˇ
12362 "#
12363 .unindent(),
12364 );
12365
12366 cx.update_editor(|editor, window, cx| {
12367 editor.handle_input("<", window, cx);
12368 });
12369 cx.assert_editor_state(
12370 &r#"
12371 <body><ˇ>
12372 <script>
12373 var x = 1;<ˇ
12374 </script>
12375 </body><ˇ>
12376 "#
12377 .unindent(),
12378 );
12379
12380 // When backspacing, the closing angle brackets are removed.
12381 cx.update_editor(|editor, window, cx| {
12382 editor.backspace(&Backspace, window, cx);
12383 });
12384 cx.assert_editor_state(
12385 &r#"
12386 <body>ˇ
12387 <script>
12388 var x = 1;ˇ
12389 </script>
12390 </body>ˇ
12391 "#
12392 .unindent(),
12393 );
12394
12395 // Block comments autoclose in JavaScript, but not HTML.
12396 cx.update_editor(|editor, window, cx| {
12397 editor.handle_input("/", window, cx);
12398 editor.handle_input("*", window, cx);
12399 });
12400 cx.assert_editor_state(
12401 &r#"
12402 <body>/*ˇ
12403 <script>
12404 var x = 1;/*ˇ */
12405 </script>
12406 </body>/*ˇ
12407 "#
12408 .unindent(),
12409 );
12410}
12411
12412#[gpui::test]
12413async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
12414 init_test(cx, |_| {});
12415
12416 let mut cx = EditorTestContext::new(cx).await;
12417
12418 let rust_language = Arc::new(
12419 Language::new(
12420 LanguageConfig {
12421 name: "Rust".into(),
12422 brackets: serde_json::from_value(json!([
12423 { "start": "{", "end": "}", "close": true, "newline": true },
12424 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
12425 ]))
12426 .unwrap(),
12427 autoclose_before: "})]>".into(),
12428 ..Default::default()
12429 },
12430 Some(tree_sitter_rust::LANGUAGE.into()),
12431 )
12432 .with_override_query("(string_literal) @string")
12433 .unwrap(),
12434 );
12435
12436 cx.language_registry().add(rust_language.clone());
12437 cx.update_buffer(|buffer, cx| {
12438 buffer.set_language(Some(rust_language), cx);
12439 });
12440
12441 cx.set_state(
12442 &r#"
12443 let x = ˇ
12444 "#
12445 .unindent(),
12446 );
12447
12448 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
12449 cx.update_editor(|editor, window, cx| {
12450 editor.handle_input("\"", window, cx);
12451 });
12452 cx.assert_editor_state(
12453 &r#"
12454 let x = "ˇ"
12455 "#
12456 .unindent(),
12457 );
12458
12459 // Inserting another quotation mark. The cursor moves across the existing
12460 // automatically-inserted quotation mark.
12461 cx.update_editor(|editor, window, cx| {
12462 editor.handle_input("\"", window, cx);
12463 });
12464 cx.assert_editor_state(
12465 &r#"
12466 let x = ""ˇ
12467 "#
12468 .unindent(),
12469 );
12470
12471 // Reset
12472 cx.set_state(
12473 &r#"
12474 let x = ˇ
12475 "#
12476 .unindent(),
12477 );
12478
12479 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
12480 cx.update_editor(|editor, window, cx| {
12481 editor.handle_input("\"", window, cx);
12482 editor.handle_input(" ", window, cx);
12483 editor.move_left(&Default::default(), window, cx);
12484 editor.handle_input("\\", window, cx);
12485 editor.handle_input("\"", window, cx);
12486 });
12487 cx.assert_editor_state(
12488 &r#"
12489 let x = "\"ˇ "
12490 "#
12491 .unindent(),
12492 );
12493
12494 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
12495 // mark. Nothing is inserted.
12496 cx.update_editor(|editor, window, cx| {
12497 editor.move_right(&Default::default(), window, cx);
12498 editor.handle_input("\"", window, cx);
12499 });
12500 cx.assert_editor_state(
12501 &r#"
12502 let x = "\" "ˇ
12503 "#
12504 .unindent(),
12505 );
12506}
12507
12508#[gpui::test]
12509async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
12510 init_test(cx, |_| {});
12511
12512 let mut cx = EditorTestContext::new(cx).await;
12513 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
12514
12515 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12516
12517 // Double quote inside single-quoted string
12518 cx.set_state(indoc! {r#"
12519 def main():
12520 items = ['"', ˇ]
12521 "#});
12522 cx.update_editor(|editor, window, cx| {
12523 editor.handle_input("\"", window, cx);
12524 });
12525 cx.assert_editor_state(indoc! {r#"
12526 def main():
12527 items = ['"', "ˇ"]
12528 "#});
12529
12530 // Two double quotes inside single-quoted string
12531 cx.set_state(indoc! {r#"
12532 def main():
12533 items = ['""', ˇ]
12534 "#});
12535 cx.update_editor(|editor, window, cx| {
12536 editor.handle_input("\"", window, cx);
12537 });
12538 cx.assert_editor_state(indoc! {r#"
12539 def main():
12540 items = ['""', "ˇ"]
12541 "#});
12542
12543 // Single quote inside double-quoted string
12544 cx.set_state(indoc! {r#"
12545 def main():
12546 items = ["'", ˇ]
12547 "#});
12548 cx.update_editor(|editor, window, cx| {
12549 editor.handle_input("'", window, cx);
12550 });
12551 cx.assert_editor_state(indoc! {r#"
12552 def main():
12553 items = ["'", 'ˇ']
12554 "#});
12555
12556 // Two single quotes inside double-quoted string
12557 cx.set_state(indoc! {r#"
12558 def main():
12559 items = ["''", ˇ]
12560 "#});
12561 cx.update_editor(|editor, window, cx| {
12562 editor.handle_input("'", window, cx);
12563 });
12564 cx.assert_editor_state(indoc! {r#"
12565 def main():
12566 items = ["''", 'ˇ']
12567 "#});
12568
12569 // Mixed quotes on same line
12570 cx.set_state(indoc! {r#"
12571 def main():
12572 items = ['"""', "'''''", ˇ]
12573 "#});
12574 cx.update_editor(|editor, window, cx| {
12575 editor.handle_input("\"", window, cx);
12576 });
12577 cx.assert_editor_state(indoc! {r#"
12578 def main():
12579 items = ['"""', "'''''", "ˇ"]
12580 "#});
12581 cx.update_editor(|editor, window, cx| {
12582 editor.move_right(&MoveRight, window, cx);
12583 });
12584 cx.update_editor(|editor, window, cx| {
12585 editor.handle_input(", ", window, cx);
12586 });
12587 cx.update_editor(|editor, window, cx| {
12588 editor.handle_input("'", window, cx);
12589 });
12590 cx.assert_editor_state(indoc! {r#"
12591 def main():
12592 items = ['"""', "'''''", "", 'ˇ']
12593 "#});
12594}
12595
12596#[gpui::test]
12597async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
12598 init_test(cx, |_| {});
12599
12600 let mut cx = EditorTestContext::new(cx).await;
12601 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
12602 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12603
12604 cx.set_state(indoc! {r#"
12605 def main():
12606 items = ["🎉", ˇ]
12607 "#});
12608 cx.update_editor(|editor, window, cx| {
12609 editor.handle_input("\"", window, cx);
12610 });
12611 cx.assert_editor_state(indoc! {r#"
12612 def main():
12613 items = ["🎉", "ˇ"]
12614 "#});
12615}
12616
12617#[gpui::test]
12618async fn test_surround_with_pair(cx: &mut TestAppContext) {
12619 init_test(cx, |_| {});
12620
12621 let language = Arc::new(Language::new(
12622 LanguageConfig {
12623 brackets: BracketPairConfig {
12624 pairs: vec![
12625 BracketPair {
12626 start: "{".to_string(),
12627 end: "}".to_string(),
12628 close: true,
12629 surround: true,
12630 newline: true,
12631 },
12632 BracketPair {
12633 start: "/* ".to_string(),
12634 end: "*/".to_string(),
12635 close: true,
12636 surround: true,
12637 ..Default::default()
12638 },
12639 ],
12640 ..Default::default()
12641 },
12642 ..Default::default()
12643 },
12644 Some(tree_sitter_rust::LANGUAGE.into()),
12645 ));
12646
12647 let text = r#"
12648 a
12649 b
12650 c
12651 "#
12652 .unindent();
12653
12654 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12655 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12656 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12657 editor
12658 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12659 .await;
12660
12661 editor.update_in(cx, |editor, window, cx| {
12662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12663 s.select_display_ranges([
12664 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12665 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12666 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
12667 ])
12668 });
12669
12670 editor.handle_input("{", window, cx);
12671 editor.handle_input("{", window, cx);
12672 editor.handle_input("{", window, cx);
12673 assert_eq!(
12674 editor.text(cx),
12675 "
12676 {{{a}}}
12677 {{{b}}}
12678 {{{c}}}
12679 "
12680 .unindent()
12681 );
12682 assert_eq!(
12683 display_ranges(editor, cx),
12684 [
12685 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
12686 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
12687 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
12688 ]
12689 );
12690
12691 editor.undo(&Undo, window, cx);
12692 editor.undo(&Undo, window, cx);
12693 editor.undo(&Undo, window, cx);
12694 assert_eq!(
12695 editor.text(cx),
12696 "
12697 a
12698 b
12699 c
12700 "
12701 .unindent()
12702 );
12703 assert_eq!(
12704 display_ranges(editor, cx),
12705 [
12706 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12707 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12708 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12709 ]
12710 );
12711
12712 // Ensure inserting the first character of a multi-byte bracket pair
12713 // doesn't surround the selections with the bracket.
12714 editor.handle_input("/", window, cx);
12715 assert_eq!(
12716 editor.text(cx),
12717 "
12718 /
12719 /
12720 /
12721 "
12722 .unindent()
12723 );
12724 assert_eq!(
12725 display_ranges(editor, cx),
12726 [
12727 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12728 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12729 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12730 ]
12731 );
12732
12733 editor.undo(&Undo, window, cx);
12734 assert_eq!(
12735 editor.text(cx),
12736 "
12737 a
12738 b
12739 c
12740 "
12741 .unindent()
12742 );
12743 assert_eq!(
12744 display_ranges(editor, cx),
12745 [
12746 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12747 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12748 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12749 ]
12750 );
12751
12752 // Ensure inserting the last character of a multi-byte bracket pair
12753 // doesn't surround the selections with the bracket.
12754 editor.handle_input("*", window, cx);
12755 assert_eq!(
12756 editor.text(cx),
12757 "
12758 *
12759 *
12760 *
12761 "
12762 .unindent()
12763 );
12764 assert_eq!(
12765 display_ranges(editor, cx),
12766 [
12767 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12768 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12769 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12770 ]
12771 );
12772 });
12773}
12774
12775#[gpui::test]
12776async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
12777 init_test(cx, |_| {});
12778
12779 let language = Arc::new(Language::new(
12780 LanguageConfig {
12781 brackets: BracketPairConfig {
12782 pairs: vec![BracketPair {
12783 start: "{".to_string(),
12784 end: "}".to_string(),
12785 close: true,
12786 surround: true,
12787 newline: true,
12788 }],
12789 ..Default::default()
12790 },
12791 autoclose_before: "}".to_string(),
12792 ..Default::default()
12793 },
12794 Some(tree_sitter_rust::LANGUAGE.into()),
12795 ));
12796
12797 let text = r#"
12798 a
12799 b
12800 c
12801 "#
12802 .unindent();
12803
12804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12806 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12807 editor
12808 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12809 .await;
12810
12811 editor.update_in(cx, |editor, window, cx| {
12812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12813 s.select_ranges([
12814 Point::new(0, 1)..Point::new(0, 1),
12815 Point::new(1, 1)..Point::new(1, 1),
12816 Point::new(2, 1)..Point::new(2, 1),
12817 ])
12818 });
12819
12820 editor.handle_input("{", window, cx);
12821 editor.handle_input("{", window, cx);
12822 editor.handle_input("_", window, cx);
12823 assert_eq!(
12824 editor.text(cx),
12825 "
12826 a{{_}}
12827 b{{_}}
12828 c{{_}}
12829 "
12830 .unindent()
12831 );
12832 assert_eq!(
12833 editor
12834 .selections
12835 .ranges::<Point>(&editor.display_snapshot(cx)),
12836 [
12837 Point::new(0, 4)..Point::new(0, 4),
12838 Point::new(1, 4)..Point::new(1, 4),
12839 Point::new(2, 4)..Point::new(2, 4)
12840 ]
12841 );
12842
12843 editor.backspace(&Default::default(), window, cx);
12844 editor.backspace(&Default::default(), window, cx);
12845 assert_eq!(
12846 editor.text(cx),
12847 "
12848 a{}
12849 b{}
12850 c{}
12851 "
12852 .unindent()
12853 );
12854 assert_eq!(
12855 editor
12856 .selections
12857 .ranges::<Point>(&editor.display_snapshot(cx)),
12858 [
12859 Point::new(0, 2)..Point::new(0, 2),
12860 Point::new(1, 2)..Point::new(1, 2),
12861 Point::new(2, 2)..Point::new(2, 2)
12862 ]
12863 );
12864
12865 editor.delete_to_previous_word_start(&Default::default(), window, cx);
12866 assert_eq!(
12867 editor.text(cx),
12868 "
12869 a
12870 b
12871 c
12872 "
12873 .unindent()
12874 );
12875 assert_eq!(
12876 editor
12877 .selections
12878 .ranges::<Point>(&editor.display_snapshot(cx)),
12879 [
12880 Point::new(0, 1)..Point::new(0, 1),
12881 Point::new(1, 1)..Point::new(1, 1),
12882 Point::new(2, 1)..Point::new(2, 1)
12883 ]
12884 );
12885 });
12886}
12887
12888#[gpui::test]
12889async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
12890 init_test(cx, |settings| {
12891 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
12892 });
12893
12894 let mut cx = EditorTestContext::new(cx).await;
12895
12896 let language = Arc::new(Language::new(
12897 LanguageConfig {
12898 brackets: BracketPairConfig {
12899 pairs: vec![
12900 BracketPair {
12901 start: "{".to_string(),
12902 end: "}".to_string(),
12903 close: true,
12904 surround: true,
12905 newline: true,
12906 },
12907 BracketPair {
12908 start: "(".to_string(),
12909 end: ")".to_string(),
12910 close: true,
12911 surround: true,
12912 newline: true,
12913 },
12914 BracketPair {
12915 start: "[".to_string(),
12916 end: "]".to_string(),
12917 close: false,
12918 surround: true,
12919 newline: true,
12920 },
12921 ],
12922 ..Default::default()
12923 },
12924 autoclose_before: "})]".to_string(),
12925 ..Default::default()
12926 },
12927 Some(tree_sitter_rust::LANGUAGE.into()),
12928 ));
12929
12930 cx.language_registry().add(language.clone());
12931 cx.update_buffer(|buffer, cx| {
12932 buffer.set_language(Some(language), cx);
12933 });
12934
12935 cx.set_state(
12936 &"
12937 {(ˇ)}
12938 [[ˇ]]
12939 {(ˇ)}
12940 "
12941 .unindent(),
12942 );
12943
12944 cx.update_editor(|editor, window, cx| {
12945 editor.backspace(&Default::default(), window, cx);
12946 editor.backspace(&Default::default(), window, cx);
12947 });
12948
12949 cx.assert_editor_state(
12950 &"
12951 ˇ
12952 ˇ]]
12953 ˇ
12954 "
12955 .unindent(),
12956 );
12957
12958 cx.update_editor(|editor, window, cx| {
12959 editor.handle_input("{", window, cx);
12960 editor.handle_input("{", window, cx);
12961 editor.move_right(&MoveRight, window, cx);
12962 editor.move_right(&MoveRight, window, cx);
12963 editor.move_left(&MoveLeft, window, cx);
12964 editor.move_left(&MoveLeft, window, cx);
12965 editor.backspace(&Default::default(), window, cx);
12966 });
12967
12968 cx.assert_editor_state(
12969 &"
12970 {ˇ}
12971 {ˇ}]]
12972 {ˇ}
12973 "
12974 .unindent(),
12975 );
12976
12977 cx.update_editor(|editor, window, cx| {
12978 editor.backspace(&Default::default(), window, cx);
12979 });
12980
12981 cx.assert_editor_state(
12982 &"
12983 ˇ
12984 ˇ]]
12985 ˇ
12986 "
12987 .unindent(),
12988 );
12989}
12990
12991#[gpui::test]
12992async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
12993 init_test(cx, |_| {});
12994
12995 let language = Arc::new(Language::new(
12996 LanguageConfig::default(),
12997 Some(tree_sitter_rust::LANGUAGE.into()),
12998 ));
12999
13000 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
13001 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13002 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13003 editor
13004 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13005 .await;
13006
13007 editor.update_in(cx, |editor, window, cx| {
13008 editor.set_auto_replace_emoji_shortcode(true);
13009
13010 editor.handle_input("Hello ", window, cx);
13011 editor.handle_input(":wave", window, cx);
13012 assert_eq!(editor.text(cx), "Hello :wave".unindent());
13013
13014 editor.handle_input(":", window, cx);
13015 assert_eq!(editor.text(cx), "Hello 👋".unindent());
13016
13017 editor.handle_input(" :smile", window, cx);
13018 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
13019
13020 editor.handle_input(":", window, cx);
13021 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
13022
13023 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
13024 editor.handle_input(":wave", window, cx);
13025 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
13026
13027 editor.handle_input(":", window, cx);
13028 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
13029
13030 editor.handle_input(":1", window, cx);
13031 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
13032
13033 editor.handle_input(":", window, cx);
13034 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
13035
13036 // Ensure shortcode does not get replaced when it is part of a word
13037 editor.handle_input(" Test:wave", window, cx);
13038 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
13039
13040 editor.handle_input(":", window, cx);
13041 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
13042
13043 editor.set_auto_replace_emoji_shortcode(false);
13044
13045 // Ensure shortcode does not get replaced when auto replace is off
13046 editor.handle_input(" :wave", window, cx);
13047 assert_eq!(
13048 editor.text(cx),
13049 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
13050 );
13051
13052 editor.handle_input(":", window, cx);
13053 assert_eq!(
13054 editor.text(cx),
13055 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
13056 );
13057 });
13058}
13059
13060#[gpui::test]
13061async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
13062 init_test(cx, |_| {});
13063
13064 let (text, insertion_ranges) = marked_text_ranges(
13065 indoc! {"
13066 ˇ
13067 "},
13068 false,
13069 );
13070
13071 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
13072 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13073
13074 _ = editor.update_in(cx, |editor, window, cx| {
13075 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
13076
13077 editor
13078 .insert_snippet(
13079 &insertion_ranges
13080 .iter()
13081 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
13082 .collect::<Vec<_>>(),
13083 snippet,
13084 window,
13085 cx,
13086 )
13087 .unwrap();
13088
13089 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
13090 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
13091 assert_eq!(editor.text(cx), expected_text);
13092 assert_eq!(
13093 editor.selections.ranges(&editor.display_snapshot(cx)),
13094 selection_ranges
13095 .iter()
13096 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
13097 .collect::<Vec<_>>()
13098 );
13099 }
13100
13101 assert(
13102 editor,
13103 cx,
13104 indoc! {"
13105 type «» =•
13106 "},
13107 );
13108
13109 assert!(editor.context_menu_visible(), "There should be a matches");
13110 });
13111}
13112
13113#[gpui::test]
13114async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
13115 init_test(cx, |_| {});
13116
13117 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
13118 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
13119 assert_eq!(editor.text(cx), expected_text);
13120 assert_eq!(
13121 editor.selections.ranges(&editor.display_snapshot(cx)),
13122 selection_ranges
13123 .iter()
13124 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
13125 .collect::<Vec<_>>()
13126 );
13127 }
13128
13129 let (text, insertion_ranges) = marked_text_ranges(
13130 indoc! {"
13131 ˇ
13132 "},
13133 false,
13134 );
13135
13136 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
13137 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13138
13139 _ = editor.update_in(cx, |editor, window, cx| {
13140 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
13141
13142 editor
13143 .insert_snippet(
13144 &insertion_ranges
13145 .iter()
13146 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
13147 .collect::<Vec<_>>(),
13148 snippet,
13149 window,
13150 cx,
13151 )
13152 .unwrap();
13153
13154 assert_state(
13155 editor,
13156 cx,
13157 indoc! {"
13158 type «» = ;•
13159 "},
13160 );
13161
13162 assert!(
13163 editor.context_menu_visible(),
13164 "Context menu should be visible for placeholder choices"
13165 );
13166
13167 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
13168
13169 assert_state(
13170 editor,
13171 cx,
13172 indoc! {"
13173 type = «»;•
13174 "},
13175 );
13176
13177 assert!(
13178 !editor.context_menu_visible(),
13179 "Context menu should be hidden after moving to next tabstop"
13180 );
13181
13182 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
13183
13184 assert_state(
13185 editor,
13186 cx,
13187 indoc! {"
13188 type = ; ˇ
13189 "},
13190 );
13191
13192 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
13193
13194 assert_state(
13195 editor,
13196 cx,
13197 indoc! {"
13198 type = ; ˇ
13199 "},
13200 );
13201 });
13202
13203 _ = editor.update_in(cx, |editor, window, cx| {
13204 editor.select_all(&SelectAll, window, cx);
13205 editor.backspace(&Backspace, window, cx);
13206
13207 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
13208 let insertion_ranges = editor
13209 .selections
13210 .all(&editor.display_snapshot(cx))
13211 .iter()
13212 .map(|s| s.range())
13213 .collect::<Vec<_>>();
13214
13215 editor
13216 .insert_snippet(&insertion_ranges, snippet, window, cx)
13217 .unwrap();
13218
13219 assert_state(editor, cx, "fn «» = value;•");
13220
13221 assert!(
13222 editor.context_menu_visible(),
13223 "Context menu should be visible for placeholder choices"
13224 );
13225
13226 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
13227
13228 assert_state(editor, cx, "fn = «valueˇ»;•");
13229
13230 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
13231
13232 assert_state(editor, cx, "fn «» = value;•");
13233
13234 assert!(
13235 editor.context_menu_visible(),
13236 "Context menu should be visible again after returning to first tabstop"
13237 );
13238
13239 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
13240
13241 assert_state(editor, cx, "fn «» = value;•");
13242 });
13243}
13244
13245#[gpui::test]
13246async fn test_snippets(cx: &mut TestAppContext) {
13247 init_test(cx, |_| {});
13248
13249 let mut cx = EditorTestContext::new(cx).await;
13250
13251 cx.set_state(indoc! {"
13252 a.ˇ b
13253 a.ˇ b
13254 a.ˇ b
13255 "});
13256
13257 cx.update_editor(|editor, window, cx| {
13258 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
13259 let insertion_ranges = editor
13260 .selections
13261 .all(&editor.display_snapshot(cx))
13262 .iter()
13263 .map(|s| s.range())
13264 .collect::<Vec<_>>();
13265 editor
13266 .insert_snippet(&insertion_ranges, snippet, window, cx)
13267 .unwrap();
13268 });
13269
13270 cx.assert_editor_state(indoc! {"
13271 a.f(«oneˇ», two, «threeˇ») b
13272 a.f(«oneˇ», two, «threeˇ») b
13273 a.f(«oneˇ», two, «threeˇ») b
13274 "});
13275
13276 // Can't move earlier than the first tab stop
13277 cx.update_editor(|editor, window, cx| {
13278 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
13279 });
13280 cx.assert_editor_state(indoc! {"
13281 a.f(«oneˇ», two, «threeˇ») b
13282 a.f(«oneˇ», two, «threeˇ») b
13283 a.f(«oneˇ», two, «threeˇ») b
13284 "});
13285
13286 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
13287 cx.assert_editor_state(indoc! {"
13288 a.f(one, «twoˇ», three) b
13289 a.f(one, «twoˇ», three) b
13290 a.f(one, «twoˇ», three) b
13291 "});
13292
13293 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
13294 cx.assert_editor_state(indoc! {"
13295 a.f(«oneˇ», two, «threeˇ») b
13296 a.f(«oneˇ», two, «threeˇ») b
13297 a.f(«oneˇ», two, «threeˇ») b
13298 "});
13299
13300 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
13301 cx.assert_editor_state(indoc! {"
13302 a.f(one, «twoˇ», three) b
13303 a.f(one, «twoˇ», three) b
13304 a.f(one, «twoˇ», three) b
13305 "});
13306 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
13307 cx.assert_editor_state(indoc! {"
13308 a.f(one, two, three)ˇ b
13309 a.f(one, two, three)ˇ b
13310 a.f(one, two, three)ˇ b
13311 "});
13312
13313 // As soon as the last tab stop is reached, snippet state is gone
13314 cx.update_editor(|editor, window, cx| {
13315 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
13316 });
13317 cx.assert_editor_state(indoc! {"
13318 a.f(one, two, three)ˇ b
13319 a.f(one, two, three)ˇ b
13320 a.f(one, two, three)ˇ b
13321 "});
13322}
13323
13324#[gpui::test]
13325async fn test_snippet_indentation(cx: &mut TestAppContext) {
13326 init_test(cx, |_| {});
13327
13328 let mut cx = EditorTestContext::new(cx).await;
13329
13330 cx.update_editor(|editor, window, cx| {
13331 let snippet = Snippet::parse(indoc! {"
13332 /*
13333 * Multiline comment with leading indentation
13334 *
13335 * $1
13336 */
13337 $0"})
13338 .unwrap();
13339 let insertion_ranges = editor
13340 .selections
13341 .all(&editor.display_snapshot(cx))
13342 .iter()
13343 .map(|s| s.range())
13344 .collect::<Vec<_>>();
13345 editor
13346 .insert_snippet(&insertion_ranges, snippet, window, cx)
13347 .unwrap();
13348 });
13349
13350 cx.assert_editor_state(indoc! {"
13351 /*
13352 * Multiline comment with leading indentation
13353 *
13354 * ˇ
13355 */
13356 "});
13357
13358 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
13359 cx.assert_editor_state(indoc! {"
13360 /*
13361 * Multiline comment with leading indentation
13362 *
13363 *•
13364 */
13365 ˇ"});
13366}
13367
13368#[gpui::test]
13369async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
13370 init_test(cx, |_| {});
13371
13372 let mut cx = EditorTestContext::new(cx).await;
13373 cx.update_editor(|editor, _, cx| {
13374 editor.project().unwrap().update(cx, |project, cx| {
13375 project.snippets().update(cx, |snippets, _cx| {
13376 let snippet = project::snippet_provider::Snippet {
13377 prefix: vec!["multi word".to_string()],
13378 body: "this is many words".to_string(),
13379 description: Some("description".to_string()),
13380 name: "multi-word snippet test".to_string(),
13381 };
13382 snippets.add_snippet_for_test(
13383 None,
13384 PathBuf::from("test_snippets.json"),
13385 vec![Arc::new(snippet)],
13386 );
13387 });
13388 })
13389 });
13390
13391 for (input_to_simulate, should_match_snippet) in [
13392 ("m", true),
13393 ("m ", true),
13394 ("m w", true),
13395 ("aa m w", true),
13396 ("aa m g", false),
13397 ] {
13398 cx.set_state("ˇ");
13399 cx.simulate_input(input_to_simulate); // fails correctly
13400
13401 cx.update_editor(|editor, _, _| {
13402 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
13403 else {
13404 assert!(!should_match_snippet); // no completions! don't even show the menu
13405 return;
13406 };
13407 assert!(context_menu.visible());
13408 let completions = context_menu.completions.borrow();
13409
13410 assert_eq!(!completions.is_empty(), should_match_snippet);
13411 });
13412 }
13413}
13414
13415#[gpui::test]
13416async fn test_document_format_during_save(cx: &mut TestAppContext) {
13417 init_test(cx, |_| {});
13418
13419 let fs = FakeFs::new(cx.executor());
13420 fs.insert_file(path!("/file.rs"), Default::default()).await;
13421
13422 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
13423
13424 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13425 language_registry.add(rust_lang());
13426 let mut fake_servers = language_registry.register_fake_lsp(
13427 "Rust",
13428 FakeLspAdapter {
13429 capabilities: lsp::ServerCapabilities {
13430 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13431 ..Default::default()
13432 },
13433 ..Default::default()
13434 },
13435 );
13436
13437 let buffer = project
13438 .update(cx, |project, cx| {
13439 project.open_local_buffer(path!("/file.rs"), cx)
13440 })
13441 .await
13442 .unwrap();
13443
13444 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13445 let (editor, cx) = cx.add_window_view(|window, cx| {
13446 build_editor_with_project(project.clone(), buffer, window, cx)
13447 });
13448 editor.update_in(cx, |editor, window, cx| {
13449 editor.set_text("one\ntwo\nthree\n", window, cx)
13450 });
13451 assert!(cx.read(|cx| editor.is_dirty(cx)));
13452
13453 let fake_server = fake_servers.next().await.unwrap();
13454
13455 {
13456 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13457 move |params, _| async move {
13458 assert_eq!(
13459 params.text_document.uri,
13460 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13461 );
13462 assert_eq!(params.options.tab_size, 4);
13463 Ok(Some(vec![lsp::TextEdit::new(
13464 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13465 ", ".to_string(),
13466 )]))
13467 },
13468 );
13469 let save = editor
13470 .update_in(cx, |editor, window, cx| {
13471 editor.save(
13472 SaveOptions {
13473 format: true,
13474 autosave: false,
13475 },
13476 project.clone(),
13477 window,
13478 cx,
13479 )
13480 })
13481 .unwrap();
13482 save.await;
13483
13484 assert_eq!(
13485 editor.update(cx, |editor, cx| editor.text(cx)),
13486 "one, two\nthree\n"
13487 );
13488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13489 }
13490
13491 {
13492 editor.update_in(cx, |editor, window, cx| {
13493 editor.set_text("one\ntwo\nthree\n", window, cx)
13494 });
13495 assert!(cx.read(|cx| editor.is_dirty(cx)));
13496
13497 // Ensure we can still save even if formatting hangs.
13498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13499 move |params, _| async move {
13500 assert_eq!(
13501 params.text_document.uri,
13502 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13503 );
13504 futures::future::pending::<()>().await;
13505 unreachable!()
13506 },
13507 );
13508 let save = editor
13509 .update_in(cx, |editor, window, cx| {
13510 editor.save(
13511 SaveOptions {
13512 format: true,
13513 autosave: false,
13514 },
13515 project.clone(),
13516 window,
13517 cx,
13518 )
13519 })
13520 .unwrap();
13521 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13522 save.await;
13523 assert_eq!(
13524 editor.update(cx, |editor, cx| editor.text(cx)),
13525 "one\ntwo\nthree\n"
13526 );
13527 }
13528
13529 // Set rust language override and assert overridden tabsize is sent to language server
13530 update_test_language_settings(cx, &|settings| {
13531 settings.languages.0.insert(
13532 "Rust".into(),
13533 LanguageSettingsContent {
13534 tab_size: NonZeroU32::new(8),
13535 ..Default::default()
13536 },
13537 );
13538 });
13539
13540 {
13541 editor.update_in(cx, |editor, window, cx| {
13542 editor.set_text("somehting_new\n", window, cx)
13543 });
13544 assert!(cx.read(|cx| editor.is_dirty(cx)));
13545 let _formatting_request_signal = fake_server
13546 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13547 assert_eq!(
13548 params.text_document.uri,
13549 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13550 );
13551 assert_eq!(params.options.tab_size, 8);
13552 Ok(Some(vec![]))
13553 });
13554 let save = editor
13555 .update_in(cx, |editor, window, cx| {
13556 editor.save(
13557 SaveOptions {
13558 format: true,
13559 autosave: false,
13560 },
13561 project.clone(),
13562 window,
13563 cx,
13564 )
13565 })
13566 .unwrap();
13567 save.await;
13568 }
13569}
13570
13571#[gpui::test]
13572async fn test_auto_formatter_skips_server_without_formatting(cx: &mut TestAppContext) {
13573 init_test(cx, |_| {});
13574
13575 let fs = FakeFs::new(cx.executor());
13576 fs.insert_file(path!("/file.rs"), Default::default()).await;
13577
13578 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
13579
13580 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13581 language_registry.add(rust_lang());
13582
13583 // First server: no formatting capability
13584 let mut no_format_servers = language_registry.register_fake_lsp(
13585 "Rust",
13586 FakeLspAdapter {
13587 name: "no-format-server",
13588 capabilities: lsp::ServerCapabilities {
13589 completion_provider: Some(lsp::CompletionOptions::default()),
13590 ..Default::default()
13591 },
13592 ..Default::default()
13593 },
13594 );
13595
13596 // Second server: has formatting capability
13597 let mut format_servers = language_registry.register_fake_lsp(
13598 "Rust",
13599 FakeLspAdapter {
13600 name: "format-server",
13601 capabilities: lsp::ServerCapabilities {
13602 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13603 ..Default::default()
13604 },
13605 ..Default::default()
13606 },
13607 );
13608
13609 let buffer = project
13610 .update(cx, |project, cx| {
13611 project.open_local_buffer(path!("/file.rs"), cx)
13612 })
13613 .await
13614 .unwrap();
13615
13616 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13617 let (editor, cx) = cx.add_window_view(|window, cx| {
13618 build_editor_with_project(project.clone(), buffer, window, cx)
13619 });
13620 editor.update_in(cx, |editor, window, cx| {
13621 editor.set_text("one\ntwo\nthree\n", window, cx)
13622 });
13623
13624 let _no_format_server = no_format_servers.next().await.unwrap();
13625 let format_server = format_servers.next().await.unwrap();
13626
13627 format_server.set_request_handler::<lsp::request::Formatting, _, _>(
13628 move |params, _| async move {
13629 assert_eq!(
13630 params.text_document.uri,
13631 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13632 );
13633 Ok(Some(vec![lsp::TextEdit::new(
13634 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13635 ", ".to_string(),
13636 )]))
13637 },
13638 );
13639
13640 let save = editor
13641 .update_in(cx, |editor, window, cx| {
13642 editor.save(
13643 SaveOptions {
13644 format: true,
13645 autosave: false,
13646 },
13647 project.clone(),
13648 window,
13649 cx,
13650 )
13651 })
13652 .unwrap();
13653 save.await;
13654
13655 assert_eq!(
13656 editor.update(cx, |editor, cx| editor.text(cx)),
13657 "one, two\nthree\n"
13658 );
13659}
13660
13661#[gpui::test]
13662async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
13663 init_test(cx, |settings| {
13664 settings.defaults.ensure_final_newline_on_save = Some(false);
13665 });
13666
13667 let fs = FakeFs::new(cx.executor());
13668 fs.insert_file(path!("/file.txt"), "foo".into()).await;
13669
13670 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
13671
13672 let buffer = project
13673 .update(cx, |project, cx| {
13674 project.open_local_buffer(path!("/file.txt"), cx)
13675 })
13676 .await
13677 .unwrap();
13678
13679 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13680 let (editor, cx) = cx.add_window_view(|window, cx| {
13681 build_editor_with_project(project.clone(), buffer, window, cx)
13682 });
13683 editor.update_in(cx, |editor, window, cx| {
13684 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
13685 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13686 });
13687 });
13688 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13689
13690 editor.update_in(cx, |editor, window, cx| {
13691 editor.handle_input("\n", window, cx)
13692 });
13693 cx.run_until_parked();
13694 save(&editor, &project, cx).await;
13695 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13696
13697 editor.update_in(cx, |editor, window, cx| {
13698 editor.undo(&Default::default(), window, cx);
13699 });
13700 save(&editor, &project, cx).await;
13701 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13702
13703 editor.update_in(cx, |editor, window, cx| {
13704 editor.redo(&Default::default(), window, cx);
13705 });
13706 cx.run_until_parked();
13707 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13708
13709 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
13710 let save = editor
13711 .update_in(cx, |editor, window, cx| {
13712 editor.save(
13713 SaveOptions {
13714 format: true,
13715 autosave: false,
13716 },
13717 project.clone(),
13718 window,
13719 cx,
13720 )
13721 })
13722 .unwrap();
13723 save.await;
13724 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13725 }
13726}
13727
13728#[gpui::test]
13729async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
13730 init_test(cx, |_| {});
13731
13732 let cols = 4;
13733 let rows = 10;
13734 let sample_text_1 = sample_text(rows, cols, 'a');
13735 assert_eq!(
13736 sample_text_1,
13737 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13738 );
13739 let sample_text_2 = sample_text(rows, cols, 'l');
13740 assert_eq!(
13741 sample_text_2,
13742 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13743 );
13744 let sample_text_3 = sample_text(rows, cols, 'v').replace('\u{7f}', ".");
13745 assert_eq!(
13746 sample_text_3,
13747 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n...."
13748 );
13749
13750 let fs = FakeFs::new(cx.executor());
13751 fs.insert_tree(
13752 path!("/a"),
13753 json!({
13754 "main.rs": sample_text_1,
13755 "other.rs": sample_text_2,
13756 "lib.rs": sample_text_3,
13757 }),
13758 )
13759 .await;
13760
13761 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13762 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
13763 let cx = &mut VisualTestContext::from_window(*window, cx);
13764
13765 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13766 language_registry.add(rust_lang());
13767 let mut fake_servers = language_registry.register_fake_lsp(
13768 "Rust",
13769 FakeLspAdapter {
13770 capabilities: lsp::ServerCapabilities {
13771 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13772 ..Default::default()
13773 },
13774 ..Default::default()
13775 },
13776 );
13777
13778 let worktree = project.update(cx, |project, cx| {
13779 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
13780 assert_eq!(worktrees.len(), 1);
13781 worktrees.pop().unwrap()
13782 });
13783 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
13784
13785 let buffer_1 = project
13786 .update(cx, |project, cx| {
13787 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
13788 })
13789 .await
13790 .unwrap();
13791 let buffer_2 = project
13792 .update(cx, |project, cx| {
13793 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
13794 })
13795 .await
13796 .unwrap();
13797 let buffer_3 = project
13798 .update(cx, |project, cx| {
13799 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
13800 })
13801 .await
13802 .unwrap();
13803
13804 let multi_buffer = cx.new(|cx| {
13805 let mut multi_buffer = MultiBuffer::new(ReadWrite);
13806 multi_buffer.set_excerpts_for_path(
13807 PathKey::sorted(0),
13808 buffer_1.clone(),
13809 [
13810 Point::new(0, 0)..Point::new(2, 4),
13811 Point::new(5, 0)..Point::new(6, 4),
13812 Point::new(9, 0)..Point::new(9, 4),
13813 ],
13814 0,
13815 cx,
13816 );
13817 multi_buffer.set_excerpts_for_path(
13818 PathKey::sorted(1),
13819 buffer_2.clone(),
13820 [
13821 Point::new(0, 0)..Point::new(2, 4),
13822 Point::new(5, 0)..Point::new(6, 4),
13823 Point::new(9, 0)..Point::new(9, 4),
13824 ],
13825 0,
13826 cx,
13827 );
13828 multi_buffer.set_excerpts_for_path(
13829 PathKey::sorted(2),
13830 buffer_3.clone(),
13831 [
13832 Point::new(0, 0)..Point::new(2, 4),
13833 Point::new(5, 0)..Point::new(6, 4),
13834 Point::new(9, 0)..Point::new(9, 4),
13835 ],
13836 0,
13837 cx,
13838 );
13839 assert_eq!(multi_buffer.read(cx).excerpts().count(), 9);
13840 multi_buffer
13841 });
13842 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13843 Editor::new(
13844 EditorMode::full(),
13845 multi_buffer,
13846 Some(project.clone()),
13847 window,
13848 cx,
13849 )
13850 });
13851
13852 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13853 let a = editor.text(cx).find("aaaa").unwrap();
13854 editor.change_selections(
13855 SelectionEffects::scroll(Autoscroll::Next),
13856 window,
13857 cx,
13858 |s| s.select_ranges(Some(MultiBufferOffset(a + 1)..MultiBufferOffset(a + 2))),
13859 );
13860 editor.insert("|one|two|three|", window, cx);
13861 });
13862 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13863 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13864 let n = editor.text(cx).find("nnnn").unwrap();
13865 editor.change_selections(
13866 SelectionEffects::scroll(Autoscroll::Next),
13867 window,
13868 cx,
13869 |s| s.select_ranges(Some(MultiBufferOffset(n + 4)..MultiBufferOffset(n + 14))),
13870 );
13871 editor.insert("|four|five|six|", window, cx);
13872 });
13873 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13874
13875 // First two buffers should be edited, but not the third one.
13876 pretty_assertions::assert_eq!(
13877 editor_content_with_blocks(&multi_buffer_editor, cx),
13878 indoc! {"
13879 § main.rs
13880 § -----
13881 a|one|two|three|aa
13882 bbbb
13883 cccc
13884 § -----
13885 ffff
13886 gggg
13887 § -----
13888 jjjj
13889 § other.rs
13890 § -----
13891 llll
13892 mmmm
13893 nnnn|four|five|six|
13894 § -----
13895
13896 § -----
13897 uuuu
13898 § lib.rs
13899 § -----
13900 vvvv
13901 wwww
13902 xxxx
13903 § -----
13904 {{{{
13905 ||||
13906 § -----
13907 ...."}
13908 );
13909 buffer_1.update(cx, |buffer, _| {
13910 assert!(buffer.is_dirty());
13911 assert_eq!(
13912 buffer.text(),
13913 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
13914 )
13915 });
13916 buffer_2.update(cx, |buffer, _| {
13917 assert!(buffer.is_dirty());
13918 assert_eq!(
13919 buffer.text(),
13920 "llll\nmmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu",
13921 )
13922 });
13923 buffer_3.update(cx, |buffer, _| {
13924 assert!(!buffer.is_dirty());
13925 assert_eq!(buffer.text(), sample_text_3,)
13926 });
13927 cx.executor().run_until_parked();
13928
13929 let save = multi_buffer_editor
13930 .update_in(cx, |editor, window, cx| {
13931 editor.save(
13932 SaveOptions {
13933 format: true,
13934 autosave: false,
13935 },
13936 project.clone(),
13937 window,
13938 cx,
13939 )
13940 })
13941 .unwrap();
13942
13943 let fake_server = fake_servers.next().await.unwrap();
13944 fake_server
13945 .server
13946 .on_request::<lsp::request::Formatting, _, _>(move |_params, _| async move {
13947 Ok(Some(vec![lsp::TextEdit::new(
13948 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13949 "[formatted]".to_string(),
13950 )]))
13951 })
13952 .detach();
13953 save.await;
13954
13955 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
13956 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
13957 assert_eq!(
13958 editor_content_with_blocks(&multi_buffer_editor, cx),
13959 indoc! {"
13960 § main.rs
13961 § -----
13962 a|o[formatted]bbbb
13963 cccc
13964 § -----
13965 ffff
13966 gggg
13967 § -----
13968 jjjj
13969
13970 § other.rs
13971 § -----
13972 lll[formatted]mmmm
13973 nnnn|four|five|six|
13974 § -----
13975
13976 § -----
13977 uuuu
13978
13979 § lib.rs
13980 § -----
13981 vvvv
13982 wwww
13983 xxxx
13984 § -----
13985 {{{{
13986 ||||
13987 § -----
13988 ...."}
13989 );
13990 buffer_1.update(cx, |buffer, _| {
13991 assert!(!buffer.is_dirty());
13992 assert_eq!(
13993 buffer.text(),
13994 "a|o[formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
13995 )
13996 });
13997 // Diff < left / right > :
13998 // lll[formatted]mmmm
13999 // <nnnn|four|five|six|
14000 // <oooo
14001 // >nnnn|four|five|six|oooo
14002 // pppp
14003 // <
14004 // ssss
14005 // tttt
14006 // uuuu
14007
14008 buffer_2.update(cx, |buffer, _| {
14009 assert!(!buffer.is_dirty());
14010 assert_eq!(
14011 buffer.text(),
14012 "lll[formatted]mmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu\n",
14013 )
14014 });
14015 buffer_3.update(cx, |buffer, _| {
14016 assert!(!buffer.is_dirty());
14017 assert_eq!(buffer.text(), sample_text_3,)
14018 });
14019}
14020
14021#[gpui::test]
14022async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
14023 init_test(cx, |_| {});
14024
14025 let fs = FakeFs::new(cx.executor());
14026 fs.insert_tree(
14027 path!("/dir"),
14028 json!({
14029 "file1.rs": "fn main() { println!(\"hello\"); }",
14030 "file2.rs": "fn test() { println!(\"test\"); }",
14031 "file3.rs": "fn other() { println!(\"other\"); }\n",
14032 }),
14033 )
14034 .await;
14035
14036 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
14037 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
14038 let cx = &mut VisualTestContext::from_window(*window, cx);
14039
14040 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14041 language_registry.add(rust_lang());
14042
14043 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
14044 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14045
14046 // Open three buffers
14047 let buffer_1 = project
14048 .update(cx, |project, cx| {
14049 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
14050 })
14051 .await
14052 .unwrap();
14053 let buffer_2 = project
14054 .update(cx, |project, cx| {
14055 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
14056 })
14057 .await
14058 .unwrap();
14059 let buffer_3 = project
14060 .update(cx, |project, cx| {
14061 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
14062 })
14063 .await
14064 .unwrap();
14065
14066 // Create a multi-buffer with all three buffers
14067 let multi_buffer = cx.new(|cx| {
14068 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14069 multi_buffer.set_excerpts_for_path(
14070 PathKey::sorted(0),
14071 buffer_1.clone(),
14072 [Point::new(0, 0)..Point::new(1, 0)],
14073 0,
14074 cx,
14075 );
14076 multi_buffer.set_excerpts_for_path(
14077 PathKey::sorted(1),
14078 buffer_2.clone(),
14079 [Point::new(0, 0)..Point::new(1, 0)],
14080 0,
14081 cx,
14082 );
14083 multi_buffer.set_excerpts_for_path(
14084 PathKey::sorted(2),
14085 buffer_3.clone(),
14086 [Point::new(0, 0)..Point::new(1, 0)],
14087 0,
14088 cx,
14089 );
14090 multi_buffer
14091 });
14092
14093 let editor = cx.new_window_entity(|window, cx| {
14094 Editor::new(
14095 EditorMode::full(),
14096 multi_buffer,
14097 Some(project.clone()),
14098 window,
14099 cx,
14100 )
14101 });
14102
14103 // Edit only the first buffer
14104 editor.update_in(cx, |editor, window, cx| {
14105 editor.change_selections(
14106 SelectionEffects::scroll(Autoscroll::Next),
14107 window,
14108 cx,
14109 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
14110 );
14111 editor.insert("// edited", window, cx);
14112 });
14113
14114 // Verify that only buffer 1 is dirty
14115 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
14116 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
14117 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
14118
14119 // Get write counts after file creation (files were created with initial content)
14120 // We expect each file to have been written once during creation
14121 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
14122 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
14123 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
14124
14125 // Perform autosave
14126 let save_task = editor.update_in(cx, |editor, window, cx| {
14127 editor.save(
14128 SaveOptions {
14129 format: true,
14130 autosave: true,
14131 },
14132 project.clone(),
14133 window,
14134 cx,
14135 )
14136 });
14137 save_task.await.unwrap();
14138
14139 // Only the dirty buffer should have been saved
14140 assert_eq!(
14141 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
14142 1,
14143 "Buffer 1 was dirty, so it should have been written once during autosave"
14144 );
14145 assert_eq!(
14146 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
14147 0,
14148 "Buffer 2 was clean, so it should not have been written during autosave"
14149 );
14150 assert_eq!(
14151 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
14152 0,
14153 "Buffer 3 was clean, so it should not have been written during autosave"
14154 );
14155
14156 // Verify buffer states after autosave
14157 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
14158 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
14159 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
14160
14161 // Now perform a manual save (format = true)
14162 let save_task = editor.update_in(cx, |editor, window, cx| {
14163 editor.save(
14164 SaveOptions {
14165 format: true,
14166 autosave: false,
14167 },
14168 project.clone(),
14169 window,
14170 cx,
14171 )
14172 });
14173 save_task.await.unwrap();
14174
14175 // During manual save, clean buffers don't get written to disk
14176 // They just get did_save called for language server notifications
14177 assert_eq!(
14178 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
14179 1,
14180 "Buffer 1 should only have been written once total (during autosave, not manual save)"
14181 );
14182 assert_eq!(
14183 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
14184 0,
14185 "Buffer 2 should not have been written at all"
14186 );
14187 assert_eq!(
14188 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
14189 0,
14190 "Buffer 3 should not have been written at all"
14191 );
14192}
14193
14194async fn setup_range_format_test(
14195 cx: &mut TestAppContext,
14196) -> (
14197 Entity<Project>,
14198 Entity<Editor>,
14199 &mut gpui::VisualTestContext,
14200 lsp::FakeLanguageServer,
14201) {
14202 init_test(cx, |_| {});
14203
14204 let fs = FakeFs::new(cx.executor());
14205 fs.insert_file(path!("/file.rs"), Default::default()).await;
14206
14207 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14208
14209 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14210 language_registry.add(rust_lang());
14211 let mut fake_servers = language_registry.register_fake_lsp(
14212 "Rust",
14213 FakeLspAdapter {
14214 capabilities: lsp::ServerCapabilities {
14215 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
14216 ..lsp::ServerCapabilities::default()
14217 },
14218 ..FakeLspAdapter::default()
14219 },
14220 );
14221
14222 let buffer = project
14223 .update(cx, |project, cx| {
14224 project.open_local_buffer(path!("/file.rs"), cx)
14225 })
14226 .await
14227 .unwrap();
14228
14229 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14230 let (editor, cx) = cx.add_window_view(|window, cx| {
14231 build_editor_with_project(project.clone(), buffer, window, cx)
14232 });
14233
14234 let fake_server = fake_servers.next().await.unwrap();
14235
14236 (project, editor, cx, fake_server)
14237}
14238
14239#[gpui::test]
14240async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
14241 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
14242
14243 editor.update_in(cx, |editor, window, cx| {
14244 editor.set_text("one\ntwo\nthree\n", window, cx)
14245 });
14246 assert!(cx.read(|cx| editor.is_dirty(cx)));
14247
14248 let save = editor
14249 .update_in(cx, |editor, window, cx| {
14250 editor.save(
14251 SaveOptions {
14252 format: true,
14253 autosave: false,
14254 },
14255 project.clone(),
14256 window,
14257 cx,
14258 )
14259 })
14260 .unwrap();
14261 fake_server
14262 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
14263 assert_eq!(
14264 params.text_document.uri,
14265 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
14266 );
14267 assert_eq!(params.options.tab_size, 4);
14268 Ok(Some(vec![lsp::TextEdit::new(
14269 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
14270 ", ".to_string(),
14271 )]))
14272 })
14273 .next()
14274 .await;
14275 save.await;
14276 assert_eq!(
14277 editor.update(cx, |editor, cx| editor.text(cx)),
14278 "one, two\nthree\n"
14279 );
14280 assert!(!cx.read(|cx| editor.is_dirty(cx)));
14281}
14282
14283#[gpui::test]
14284async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
14285 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
14286
14287 editor.update_in(cx, |editor, window, cx| {
14288 editor.set_text("one\ntwo\nthree\n", window, cx)
14289 });
14290 assert!(cx.read(|cx| editor.is_dirty(cx)));
14291
14292 // Test that save still works when formatting hangs
14293 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
14294 move |params, _| async move {
14295 assert_eq!(
14296 params.text_document.uri,
14297 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
14298 );
14299 futures::future::pending::<()>().await;
14300 unreachable!()
14301 },
14302 );
14303 let save = editor
14304 .update_in(cx, |editor, window, cx| {
14305 editor.save(
14306 SaveOptions {
14307 format: true,
14308 autosave: false,
14309 },
14310 project.clone(),
14311 window,
14312 cx,
14313 )
14314 })
14315 .unwrap();
14316 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
14317 save.await;
14318 assert_eq!(
14319 editor.update(cx, |editor, cx| editor.text(cx)),
14320 "one\ntwo\nthree\n"
14321 );
14322 assert!(!cx.read(|cx| editor.is_dirty(cx)));
14323}
14324
14325#[gpui::test]
14326async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
14327 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
14328
14329 // Buffer starts clean, no formatting should be requested
14330 let save = editor
14331 .update_in(cx, |editor, window, cx| {
14332 editor.save(
14333 SaveOptions {
14334 format: false,
14335 autosave: false,
14336 },
14337 project.clone(),
14338 window,
14339 cx,
14340 )
14341 })
14342 .unwrap();
14343 let _pending_format_request = fake_server
14344 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
14345 panic!("Should not be invoked");
14346 })
14347 .next();
14348 save.await;
14349 cx.run_until_parked();
14350}
14351
14352#[gpui::test]
14353async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
14354 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
14355
14356 // Set Rust language override and assert overridden tabsize is sent to language server
14357 update_test_language_settings(cx, &|settings| {
14358 settings.languages.0.insert(
14359 "Rust".into(),
14360 LanguageSettingsContent {
14361 tab_size: NonZeroU32::new(8),
14362 ..Default::default()
14363 },
14364 );
14365 });
14366
14367 editor.update_in(cx, |editor, window, cx| {
14368 editor.set_text("something_new\n", window, cx)
14369 });
14370 assert!(cx.read(|cx| editor.is_dirty(cx)));
14371 let save = editor
14372 .update_in(cx, |editor, window, cx| {
14373 editor.save(
14374 SaveOptions {
14375 format: true,
14376 autosave: false,
14377 },
14378 project.clone(),
14379 window,
14380 cx,
14381 )
14382 })
14383 .unwrap();
14384 fake_server
14385 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
14386 assert_eq!(
14387 params.text_document.uri,
14388 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
14389 );
14390 assert_eq!(params.options.tab_size, 8);
14391 Ok(Some(Vec::new()))
14392 })
14393 .next()
14394 .await;
14395 save.await;
14396}
14397
14398#[gpui::test]
14399async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
14400 init_test(cx, |settings| {
14401 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
14402 settings::LanguageServerFormatterSpecifier::Current,
14403 )))
14404 });
14405
14406 let fs = FakeFs::new(cx.executor());
14407 fs.insert_file(path!("/file.rs"), Default::default()).await;
14408
14409 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14410
14411 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14412 language_registry.add(Arc::new(Language::new(
14413 LanguageConfig {
14414 name: "Rust".into(),
14415 matcher: LanguageMatcher {
14416 path_suffixes: vec!["rs".to_string()],
14417 ..Default::default()
14418 },
14419 ..LanguageConfig::default()
14420 },
14421 Some(tree_sitter_rust::LANGUAGE.into()),
14422 )));
14423 update_test_language_settings(cx, &|settings| {
14424 // Enable Prettier formatting for the same buffer, and ensure
14425 // LSP is called instead of Prettier.
14426 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
14427 });
14428 let mut fake_servers = language_registry.register_fake_lsp(
14429 "Rust",
14430 FakeLspAdapter {
14431 capabilities: lsp::ServerCapabilities {
14432 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14433 ..Default::default()
14434 },
14435 ..Default::default()
14436 },
14437 );
14438
14439 let buffer = project
14440 .update(cx, |project, cx| {
14441 project.open_local_buffer(path!("/file.rs"), cx)
14442 })
14443 .await
14444 .unwrap();
14445
14446 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14447 let (editor, cx) = cx.add_window_view(|window, cx| {
14448 build_editor_with_project(project.clone(), buffer, window, cx)
14449 });
14450 editor.update_in(cx, |editor, window, cx| {
14451 editor.set_text("one\ntwo\nthree\n", window, cx)
14452 });
14453
14454 let fake_server = fake_servers.next().await.unwrap();
14455
14456 let format = editor
14457 .update_in(cx, |editor, window, cx| {
14458 editor.perform_format(
14459 project.clone(),
14460 FormatTrigger::Manual,
14461 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14462 window,
14463 cx,
14464 )
14465 })
14466 .unwrap();
14467 fake_server
14468 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
14469 assert_eq!(
14470 params.text_document.uri,
14471 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
14472 );
14473 assert_eq!(params.options.tab_size, 4);
14474 Ok(Some(vec![lsp::TextEdit::new(
14475 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
14476 ", ".to_string(),
14477 )]))
14478 })
14479 .next()
14480 .await;
14481 format.await;
14482 assert_eq!(
14483 editor.update(cx, |editor, cx| editor.text(cx)),
14484 "one, two\nthree\n"
14485 );
14486
14487 editor.update_in(cx, |editor, window, cx| {
14488 editor.set_text("one\ntwo\nthree\n", window, cx)
14489 });
14490 // Ensure we don't lock if formatting hangs.
14491 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
14492 move |params, _| async move {
14493 assert_eq!(
14494 params.text_document.uri,
14495 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
14496 );
14497 futures::future::pending::<()>().await;
14498 unreachable!()
14499 },
14500 );
14501 let format = editor
14502 .update_in(cx, |editor, window, cx| {
14503 editor.perform_format(
14504 project,
14505 FormatTrigger::Manual,
14506 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14507 window,
14508 cx,
14509 )
14510 })
14511 .unwrap();
14512 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
14513 format.await;
14514 assert_eq!(
14515 editor.update(cx, |editor, cx| editor.text(cx)),
14516 "one\ntwo\nthree\n"
14517 );
14518}
14519
14520#[gpui::test]
14521async fn test_multiple_formatters(cx: &mut TestAppContext) {
14522 init_test(cx, |settings| {
14523 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
14524 settings.defaults.formatter = Some(FormatterList::Vec(vec![
14525 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
14526 Formatter::CodeAction("code-action-1".into()),
14527 Formatter::CodeAction("code-action-2".into()),
14528 ]))
14529 });
14530
14531 let fs = FakeFs::new(cx.executor());
14532 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
14533 .await;
14534
14535 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14536 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14537 language_registry.add(rust_lang());
14538
14539 let mut fake_servers = language_registry.register_fake_lsp(
14540 "Rust",
14541 FakeLspAdapter {
14542 capabilities: lsp::ServerCapabilities {
14543 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14544 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14545 commands: vec!["the-command-for-code-action-1".into()],
14546 ..Default::default()
14547 }),
14548 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14549 ..Default::default()
14550 },
14551 ..Default::default()
14552 },
14553 );
14554
14555 let buffer = project
14556 .update(cx, |project, cx| {
14557 project.open_local_buffer(path!("/file.rs"), cx)
14558 })
14559 .await
14560 .unwrap();
14561
14562 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14563 let (editor, cx) = cx.add_window_view(|window, cx| {
14564 build_editor_with_project(project.clone(), buffer, window, cx)
14565 });
14566
14567 let fake_server = fake_servers.next().await.unwrap();
14568 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
14569 move |_params, _| async move {
14570 Ok(Some(vec![lsp::TextEdit::new(
14571 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14572 "applied-formatting\n".to_string(),
14573 )]))
14574 },
14575 );
14576 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14577 move |params, _| async move {
14578 let requested_code_actions = params.context.only.expect("Expected code action request");
14579 assert_eq!(requested_code_actions.len(), 1);
14580
14581 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
14582 let code_action = match requested_code_actions[0].as_str() {
14583 "code-action-1" => lsp::CodeAction {
14584 kind: Some("code-action-1".into()),
14585 edit: Some(lsp::WorkspaceEdit::new(
14586 [(
14587 uri,
14588 vec![lsp::TextEdit::new(
14589 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14590 "applied-code-action-1-edit\n".to_string(),
14591 )],
14592 )]
14593 .into_iter()
14594 .collect(),
14595 )),
14596 command: Some(lsp::Command {
14597 command: "the-command-for-code-action-1".into(),
14598 ..Default::default()
14599 }),
14600 ..Default::default()
14601 },
14602 "code-action-2" => lsp::CodeAction {
14603 kind: Some("code-action-2".into()),
14604 edit: Some(lsp::WorkspaceEdit::new(
14605 [(
14606 uri,
14607 vec![lsp::TextEdit::new(
14608 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14609 "applied-code-action-2-edit\n".to_string(),
14610 )],
14611 )]
14612 .into_iter()
14613 .collect(),
14614 )),
14615 ..Default::default()
14616 },
14617 req => panic!("Unexpected code action request: {:?}", req),
14618 };
14619 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14620 code_action,
14621 )]))
14622 },
14623 );
14624
14625 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
14626 move |params, _| async move { Ok(params) }
14627 });
14628
14629 let command_lock = Arc::new(futures::lock::Mutex::new(()));
14630 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14631 let fake = fake_server.clone();
14632 let lock = command_lock.clone();
14633 move |params, _| {
14634 assert_eq!(params.command, "the-command-for-code-action-1");
14635 let fake = fake.clone();
14636 let lock = lock.clone();
14637 async move {
14638 lock.lock().await;
14639 fake.server
14640 .request::<lsp::request::ApplyWorkspaceEdit>(
14641 lsp::ApplyWorkspaceEditParams {
14642 label: None,
14643 edit: lsp::WorkspaceEdit {
14644 changes: Some(
14645 [(
14646 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
14647 vec![lsp::TextEdit {
14648 range: lsp::Range::new(
14649 lsp::Position::new(0, 0),
14650 lsp::Position::new(0, 0),
14651 ),
14652 new_text: "applied-code-action-1-command\n".into(),
14653 }],
14654 )]
14655 .into_iter()
14656 .collect(),
14657 ),
14658 ..Default::default()
14659 },
14660 },
14661 DEFAULT_LSP_REQUEST_TIMEOUT,
14662 )
14663 .await
14664 .into_response()
14665 .unwrap();
14666 Ok(Some(json!(null)))
14667 }
14668 }
14669 });
14670
14671 editor
14672 .update_in(cx, |editor, window, cx| {
14673 editor.perform_format(
14674 project.clone(),
14675 FormatTrigger::Manual,
14676 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14677 window,
14678 cx,
14679 )
14680 })
14681 .unwrap()
14682 .await;
14683 editor.update(cx, |editor, cx| {
14684 assert_eq!(
14685 editor.text(cx),
14686 r#"
14687 applied-code-action-2-edit
14688 applied-code-action-1-command
14689 applied-code-action-1-edit
14690 applied-formatting
14691 one
14692 two
14693 three
14694 "#
14695 .unindent()
14696 );
14697 });
14698
14699 editor.update_in(cx, |editor, window, cx| {
14700 editor.undo(&Default::default(), window, cx);
14701 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14702 });
14703
14704 // Perform a manual edit while waiting for an LSP command
14705 // that's being run as part of a formatting code action.
14706 let lock_guard = command_lock.lock().await;
14707 let format = editor
14708 .update_in(cx, |editor, window, cx| {
14709 editor.perform_format(
14710 project.clone(),
14711 FormatTrigger::Manual,
14712 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14713 window,
14714 cx,
14715 )
14716 })
14717 .unwrap();
14718 cx.run_until_parked();
14719 editor.update(cx, |editor, cx| {
14720 assert_eq!(
14721 editor.text(cx),
14722 r#"
14723 applied-code-action-1-edit
14724 applied-formatting
14725 one
14726 two
14727 three
14728 "#
14729 .unindent()
14730 );
14731
14732 editor.buffer.update(cx, |buffer, cx| {
14733 let ix = buffer.len(cx);
14734 buffer.edit([(ix..ix, "edited\n")], None, cx);
14735 });
14736 });
14737
14738 // Allow the LSP command to proceed. Because the buffer was edited,
14739 // the second code action will not be run.
14740 drop(lock_guard);
14741 format.await;
14742 editor.update_in(cx, |editor, window, cx| {
14743 assert_eq!(
14744 editor.text(cx),
14745 r#"
14746 applied-code-action-1-command
14747 applied-code-action-1-edit
14748 applied-formatting
14749 one
14750 two
14751 three
14752 edited
14753 "#
14754 .unindent()
14755 );
14756
14757 // The manual edit is undone first, because it is the last thing the user did
14758 // (even though the command completed afterwards).
14759 editor.undo(&Default::default(), window, cx);
14760 assert_eq!(
14761 editor.text(cx),
14762 r#"
14763 applied-code-action-1-command
14764 applied-code-action-1-edit
14765 applied-formatting
14766 one
14767 two
14768 three
14769 "#
14770 .unindent()
14771 );
14772
14773 // All the formatting (including the command, which completed after the manual edit)
14774 // is undone together.
14775 editor.undo(&Default::default(), window, cx);
14776 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14777 });
14778}
14779
14780#[gpui::test]
14781async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
14782 init_test(cx, |settings| {
14783 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
14784 settings::LanguageServerFormatterSpecifier::Current,
14785 )]))
14786 });
14787
14788 let fs = FakeFs::new(cx.executor());
14789 fs.insert_file(path!("/file.ts"), Default::default()).await;
14790
14791 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14792
14793 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14794 language_registry.add(Arc::new(Language::new(
14795 LanguageConfig {
14796 name: "TypeScript".into(),
14797 matcher: LanguageMatcher {
14798 path_suffixes: vec!["ts".to_string()],
14799 ..Default::default()
14800 },
14801 ..LanguageConfig::default()
14802 },
14803 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14804 )));
14805 update_test_language_settings(cx, &|settings| {
14806 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
14807 });
14808 let mut fake_servers = language_registry.register_fake_lsp(
14809 "TypeScript",
14810 FakeLspAdapter {
14811 capabilities: lsp::ServerCapabilities {
14812 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14813 ..Default::default()
14814 },
14815 ..Default::default()
14816 },
14817 );
14818
14819 let buffer = project
14820 .update(cx, |project, cx| {
14821 project.open_local_buffer(path!("/file.ts"), cx)
14822 })
14823 .await
14824 .unwrap();
14825
14826 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14827 let (editor, cx) = cx.add_window_view(|window, cx| {
14828 build_editor_with_project(project.clone(), buffer, window, cx)
14829 });
14830 editor.update_in(cx, |editor, window, cx| {
14831 editor.set_text(
14832 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14833 window,
14834 cx,
14835 )
14836 });
14837
14838 let fake_server = fake_servers.next().await.unwrap();
14839
14840 let format = editor
14841 .update_in(cx, |editor, window, cx| {
14842 editor.perform_code_action_kind(
14843 project.clone(),
14844 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14845 window,
14846 cx,
14847 )
14848 })
14849 .unwrap();
14850 fake_server
14851 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
14852 assert_eq!(
14853 params.text_document.uri,
14854 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14855 );
14856 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14857 lsp::CodeAction {
14858 title: "Organize Imports".to_string(),
14859 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
14860 edit: Some(lsp::WorkspaceEdit {
14861 changes: Some(
14862 [(
14863 params.text_document.uri.clone(),
14864 vec![lsp::TextEdit::new(
14865 lsp::Range::new(
14866 lsp::Position::new(1, 0),
14867 lsp::Position::new(2, 0),
14868 ),
14869 "".to_string(),
14870 )],
14871 )]
14872 .into_iter()
14873 .collect(),
14874 ),
14875 ..Default::default()
14876 }),
14877 ..Default::default()
14878 },
14879 )]))
14880 })
14881 .next()
14882 .await;
14883 format.await;
14884 assert_eq!(
14885 editor.update(cx, |editor, cx| editor.text(cx)),
14886 "import { a } from 'module';\n\nconst x = a;\n"
14887 );
14888
14889 editor.update_in(cx, |editor, window, cx| {
14890 editor.set_text(
14891 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14892 window,
14893 cx,
14894 )
14895 });
14896 // Ensure we don't lock if code action hangs.
14897 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14898 move |params, _| async move {
14899 assert_eq!(
14900 params.text_document.uri,
14901 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14902 );
14903 futures::future::pending::<()>().await;
14904 unreachable!()
14905 },
14906 );
14907 let format = editor
14908 .update_in(cx, |editor, window, cx| {
14909 editor.perform_code_action_kind(
14910 project,
14911 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14912 window,
14913 cx,
14914 )
14915 })
14916 .unwrap();
14917 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
14918 format.await;
14919 assert_eq!(
14920 editor.update(cx, |editor, cx| editor.text(cx)),
14921 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
14922 );
14923}
14924
14925#[gpui::test]
14926async fn test_formatter_failure_does_not_abort_subsequent_formatters(cx: &mut TestAppContext) {
14927 init_test(cx, |settings| {
14928 settings.defaults.formatter = Some(FormatterList::Vec(vec![
14929 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
14930 Formatter::CodeAction("organize-imports".into()),
14931 ]))
14932 });
14933
14934 let fs = FakeFs::new(cx.executor());
14935 fs.insert_file(path!("/file.rs"), "fn main() {}\n".into())
14936 .await;
14937
14938 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14940 language_registry.add(rust_lang());
14941
14942 let mut fake_servers = language_registry.register_fake_lsp(
14943 "Rust",
14944 FakeLspAdapter {
14945 capabilities: lsp::ServerCapabilities {
14946 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14947 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14948 ..Default::default()
14949 },
14950 ..Default::default()
14951 },
14952 );
14953
14954 let buffer = project
14955 .update(cx, |project, cx| {
14956 project.open_local_buffer(path!("/file.rs"), cx)
14957 })
14958 .await
14959 .unwrap();
14960
14961 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14962 let (editor, cx) = cx.add_window_view(|window, cx| {
14963 build_editor_with_project(project.clone(), buffer, window, cx)
14964 });
14965
14966 let fake_server = fake_servers.next().await.unwrap();
14967
14968 // Formatter #1 (LanguageServer) returns an error to simulate failure
14969 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
14970 move |_params, _| async move { Err(anyhow::anyhow!("Simulated formatter failure")) },
14971 );
14972
14973 // Formatter #2 (CodeAction) returns a successful edit
14974 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14975 move |_params, _| async move {
14976 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
14977 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14978 lsp::CodeAction {
14979 kind: Some("organize-imports".into()),
14980 edit: Some(lsp::WorkspaceEdit::new(
14981 [(
14982 uri,
14983 vec![lsp::TextEdit::new(
14984 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14985 "use std::io;\n".to_string(),
14986 )],
14987 )]
14988 .into_iter()
14989 .collect(),
14990 )),
14991 ..Default::default()
14992 },
14993 )]))
14994 },
14995 );
14996
14997 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
14998 move |params, _| async move { Ok(params) }
14999 });
15000
15001 editor
15002 .update_in(cx, |editor, window, cx| {
15003 editor.perform_format(
15004 project.clone(),
15005 FormatTrigger::Manual,
15006 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15007 window,
15008 cx,
15009 )
15010 })
15011 .unwrap()
15012 .await;
15013
15014 // Formatter #1 (LanguageServer) failed, but formatter #2 (CodeAction) should have applied
15015 editor.update(cx, |editor, cx| {
15016 assert_eq!(editor.text(cx), "use std::io;\nfn main() {}\n");
15017 });
15018
15019 // The entire format operation should undo as one transaction
15020 editor.update_in(cx, |editor, window, cx| {
15021 editor.undo(&Default::default(), window, cx);
15022 assert_eq!(editor.text(cx), "fn main() {}\n");
15023 });
15024}
15025
15026#[gpui::test]
15027async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
15028 init_test(cx, |_| {});
15029
15030 let mut cx = EditorLspTestContext::new_rust(
15031 lsp::ServerCapabilities {
15032 document_formatting_provider: Some(lsp::OneOf::Left(true)),
15033 ..Default::default()
15034 },
15035 cx,
15036 )
15037 .await;
15038
15039 cx.set_state(indoc! {"
15040 one.twoˇ
15041 "});
15042
15043 // The format request takes a long time. When it completes, it inserts
15044 // a newline and an indent before the `.`
15045 cx.lsp
15046 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
15047 let executor = cx.background_executor().clone();
15048 async move {
15049 executor.timer(Duration::from_millis(100)).await;
15050 Ok(Some(vec![lsp::TextEdit {
15051 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
15052 new_text: "\n ".into(),
15053 }]))
15054 }
15055 });
15056
15057 // Submit a format request.
15058 let format_1 = cx
15059 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
15060 .unwrap();
15061 cx.executor().run_until_parked();
15062
15063 // Submit a second format request.
15064 let format_2 = cx
15065 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
15066 .unwrap();
15067 cx.executor().run_until_parked();
15068
15069 // Wait for both format requests to complete
15070 cx.executor().advance_clock(Duration::from_millis(200));
15071 format_1.await.unwrap();
15072 format_2.await.unwrap();
15073
15074 // The formatting edits only happens once.
15075 cx.assert_editor_state(indoc! {"
15076 one
15077 .twoˇ
15078 "});
15079}
15080
15081#[gpui::test]
15082async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
15083 init_test(cx, |settings| {
15084 settings.defaults.formatter = Some(FormatterList::default())
15085 });
15086
15087 let mut cx = EditorLspTestContext::new_rust(
15088 lsp::ServerCapabilities {
15089 document_formatting_provider: Some(lsp::OneOf::Left(true)),
15090 ..Default::default()
15091 },
15092 cx,
15093 )
15094 .await;
15095
15096 // Record which buffer changes have been sent to the language server
15097 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
15098 cx.lsp
15099 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
15100 let buffer_changes = buffer_changes.clone();
15101 move |params, _| {
15102 buffer_changes.lock().extend(
15103 params
15104 .content_changes
15105 .into_iter()
15106 .map(|e| (e.range.unwrap(), e.text)),
15107 );
15108 }
15109 });
15110 // Handle formatting requests to the language server.
15111 cx.lsp
15112 .set_request_handler::<lsp::request::Formatting, _, _>({
15113 move |_, _| {
15114 // Insert blank lines between each line of the buffer.
15115 async move {
15116 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
15117 // DidChangedTextDocument to the LSP before sending the formatting request.
15118 // assert_eq!(
15119 // &buffer_changes.lock()[1..],
15120 // &[
15121 // (
15122 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
15123 // "".into()
15124 // ),
15125 // (
15126 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
15127 // "".into()
15128 // ),
15129 // (
15130 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
15131 // "\n".into()
15132 // ),
15133 // ]
15134 // );
15135
15136 Ok(Some(vec![
15137 lsp::TextEdit {
15138 range: lsp::Range::new(
15139 lsp::Position::new(1, 0),
15140 lsp::Position::new(1, 0),
15141 ),
15142 new_text: "\n".into(),
15143 },
15144 lsp::TextEdit {
15145 range: lsp::Range::new(
15146 lsp::Position::new(2, 0),
15147 lsp::Position::new(2, 0),
15148 ),
15149 new_text: "\n".into(),
15150 },
15151 ]))
15152 }
15153 }
15154 });
15155
15156 // Set up a buffer white some trailing whitespace and no trailing newline.
15157 cx.set_state(
15158 &[
15159 "one ", //
15160 "twoˇ", //
15161 "three ", //
15162 "four", //
15163 ]
15164 .join("\n"),
15165 );
15166
15167 // Submit a format request.
15168 let format = cx
15169 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
15170 .unwrap();
15171
15172 cx.run_until_parked();
15173 // After formatting the buffer, the trailing whitespace is stripped,
15174 // a newline is appended, and the edits provided by the language server
15175 // have been applied.
15176 format.await.unwrap();
15177
15178 cx.assert_editor_state(
15179 &[
15180 "one", //
15181 "", //
15182 "twoˇ", //
15183 "", //
15184 "three", //
15185 "four", //
15186 "", //
15187 ]
15188 .join("\n"),
15189 );
15190
15191 // Undoing the formatting undoes the trailing whitespace removal, the
15192 // trailing newline, and the LSP edits.
15193 cx.update_buffer(|buffer, cx| buffer.undo(cx));
15194 cx.assert_editor_state(
15195 &[
15196 "one ", //
15197 "twoˇ", //
15198 "three ", //
15199 "four", //
15200 ]
15201 .join("\n"),
15202 );
15203}
15204
15205#[gpui::test]
15206async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
15207 cx: &mut TestAppContext,
15208) {
15209 init_test(cx, |_| {});
15210
15211 cx.update(|cx| {
15212 cx.update_global::<SettingsStore, _>(|settings, cx| {
15213 settings.update_user_settings(cx, |settings| {
15214 settings.editor.auto_signature_help = Some(true);
15215 settings.editor.hover_popover_delay = Some(DelayMs(300));
15216 });
15217 });
15218 });
15219
15220 let mut cx = EditorLspTestContext::new_rust(
15221 lsp::ServerCapabilities {
15222 signature_help_provider: Some(lsp::SignatureHelpOptions {
15223 ..Default::default()
15224 }),
15225 ..Default::default()
15226 },
15227 cx,
15228 )
15229 .await;
15230
15231 let language = Language::new(
15232 LanguageConfig {
15233 name: "Rust".into(),
15234 brackets: BracketPairConfig {
15235 pairs: vec![
15236 BracketPair {
15237 start: "{".to_string(),
15238 end: "}".to_string(),
15239 close: true,
15240 surround: true,
15241 newline: true,
15242 },
15243 BracketPair {
15244 start: "(".to_string(),
15245 end: ")".to_string(),
15246 close: true,
15247 surround: true,
15248 newline: true,
15249 },
15250 BracketPair {
15251 start: "/*".to_string(),
15252 end: " */".to_string(),
15253 close: true,
15254 surround: true,
15255 newline: true,
15256 },
15257 BracketPair {
15258 start: "[".to_string(),
15259 end: "]".to_string(),
15260 close: false,
15261 surround: false,
15262 newline: true,
15263 },
15264 BracketPair {
15265 start: "\"".to_string(),
15266 end: "\"".to_string(),
15267 close: true,
15268 surround: true,
15269 newline: false,
15270 },
15271 BracketPair {
15272 start: "<".to_string(),
15273 end: ">".to_string(),
15274 close: false,
15275 surround: true,
15276 newline: true,
15277 },
15278 ],
15279 ..Default::default()
15280 },
15281 autoclose_before: "})]".to_string(),
15282 ..Default::default()
15283 },
15284 Some(tree_sitter_rust::LANGUAGE.into()),
15285 );
15286 let language = Arc::new(language);
15287
15288 cx.language_registry().add(language.clone());
15289 cx.update_buffer(|buffer, cx| {
15290 buffer.set_language(Some(language), cx);
15291 });
15292
15293 cx.set_state(
15294 &r#"
15295 fn main() {
15296 sampleˇ
15297 }
15298 "#
15299 .unindent(),
15300 );
15301
15302 cx.update_editor(|editor, window, cx| {
15303 editor.handle_input("(", window, cx);
15304 });
15305 cx.assert_editor_state(
15306 &"
15307 fn main() {
15308 sample(ˇ)
15309 }
15310 "
15311 .unindent(),
15312 );
15313
15314 let mocked_response = lsp::SignatureHelp {
15315 signatures: vec![lsp::SignatureInformation {
15316 label: "fn sample(param1: u8, param2: u8)".to_string(),
15317 documentation: None,
15318 parameters: Some(vec![
15319 lsp::ParameterInformation {
15320 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15321 documentation: None,
15322 },
15323 lsp::ParameterInformation {
15324 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15325 documentation: None,
15326 },
15327 ]),
15328 active_parameter: None,
15329 }],
15330 active_signature: Some(0),
15331 active_parameter: Some(0),
15332 };
15333 handle_signature_help_request(&mut cx, mocked_response).await;
15334
15335 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15336 .await;
15337
15338 cx.editor(|editor, _, _| {
15339 let signature_help_state = editor.signature_help_state.popover().cloned();
15340 let signature = signature_help_state.unwrap();
15341 assert_eq!(
15342 signature.signatures[signature.current_signature].label,
15343 "fn sample(param1: u8, param2: u8)"
15344 );
15345 });
15346}
15347
15348#[gpui::test]
15349async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
15350 init_test(cx, |_| {});
15351
15352 let delay_ms = 500;
15353 cx.update(|cx| {
15354 cx.update_global::<SettingsStore, _>(|settings, cx| {
15355 settings.update_user_settings(cx, |settings| {
15356 settings.editor.auto_signature_help = Some(true);
15357 settings.editor.show_signature_help_after_edits = Some(false);
15358 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
15359 });
15360 });
15361 });
15362
15363 let mut cx = EditorLspTestContext::new_rust(
15364 lsp::ServerCapabilities {
15365 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15366 ..lsp::ServerCapabilities::default()
15367 },
15368 cx,
15369 )
15370 .await;
15371
15372 let mocked_response = lsp::SignatureHelp {
15373 signatures: vec![lsp::SignatureInformation {
15374 label: "fn sample(param1: u8)".to_string(),
15375 documentation: None,
15376 parameters: Some(vec![lsp::ParameterInformation {
15377 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15378 documentation: None,
15379 }]),
15380 active_parameter: None,
15381 }],
15382 active_signature: Some(0),
15383 active_parameter: Some(0),
15384 };
15385
15386 cx.set_state(indoc! {"
15387 fn main() {
15388 sample(ˇ);
15389 }
15390
15391 fn sample(param1: u8) {}
15392 "});
15393
15394 // Manual trigger should show immediately without delay
15395 cx.update_editor(|editor, window, cx| {
15396 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15397 });
15398 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15399 cx.run_until_parked();
15400 cx.editor(|editor, _, _| {
15401 assert!(
15402 editor.signature_help_state.is_shown(),
15403 "Manual trigger should show signature help without delay"
15404 );
15405 });
15406
15407 cx.update_editor(|editor, _, cx| {
15408 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
15409 });
15410 cx.run_until_parked();
15411 cx.editor(|editor, _, _| {
15412 assert!(!editor.signature_help_state.is_shown());
15413 });
15414
15415 // Auto trigger (cursor movement into brackets) should respect delay
15416 cx.set_state(indoc! {"
15417 fn main() {
15418 sampleˇ();
15419 }
15420
15421 fn sample(param1: u8) {}
15422 "});
15423 cx.update_editor(|editor, window, cx| {
15424 editor.move_right(&MoveRight, window, cx);
15425 });
15426 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15427 cx.run_until_parked();
15428 cx.editor(|editor, _, _| {
15429 assert!(
15430 !editor.signature_help_state.is_shown(),
15431 "Auto trigger should wait for delay before showing signature help"
15432 );
15433 });
15434
15435 cx.executor()
15436 .advance_clock(Duration::from_millis(delay_ms + 50));
15437 cx.run_until_parked();
15438 cx.editor(|editor, _, _| {
15439 assert!(
15440 editor.signature_help_state.is_shown(),
15441 "Auto trigger should show signature help after delay elapsed"
15442 );
15443 });
15444}
15445
15446#[gpui::test]
15447async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
15448 init_test(cx, |_| {});
15449
15450 let delay_ms = 500;
15451 cx.update(|cx| {
15452 cx.update_global::<SettingsStore, _>(|settings, cx| {
15453 settings.update_user_settings(cx, |settings| {
15454 settings.editor.auto_signature_help = Some(false);
15455 settings.editor.show_signature_help_after_edits = Some(true);
15456 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
15457 });
15458 });
15459 });
15460
15461 let mut cx = EditorLspTestContext::new_rust(
15462 lsp::ServerCapabilities {
15463 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15464 ..lsp::ServerCapabilities::default()
15465 },
15466 cx,
15467 )
15468 .await;
15469
15470 let language = Arc::new(Language::new(
15471 LanguageConfig {
15472 name: "Rust".into(),
15473 brackets: BracketPairConfig {
15474 pairs: vec![BracketPair {
15475 start: "(".to_string(),
15476 end: ")".to_string(),
15477 close: true,
15478 surround: true,
15479 newline: true,
15480 }],
15481 ..BracketPairConfig::default()
15482 },
15483 autoclose_before: "})".to_string(),
15484 ..LanguageConfig::default()
15485 },
15486 Some(tree_sitter_rust::LANGUAGE.into()),
15487 ));
15488 cx.language_registry().add(language.clone());
15489 cx.update_buffer(|buffer, cx| {
15490 buffer.set_language(Some(language), cx);
15491 });
15492
15493 let mocked_response = lsp::SignatureHelp {
15494 signatures: vec![lsp::SignatureInformation {
15495 label: "fn sample(param1: u8)".to_string(),
15496 documentation: None,
15497 parameters: Some(vec![lsp::ParameterInformation {
15498 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15499 documentation: None,
15500 }]),
15501 active_parameter: None,
15502 }],
15503 active_signature: Some(0),
15504 active_parameter: Some(0),
15505 };
15506
15507 cx.set_state(indoc! {"
15508 fn main() {
15509 sampleˇ
15510 }
15511 "});
15512
15513 // Typing bracket should show signature help immediately without delay
15514 cx.update_editor(|editor, window, cx| {
15515 editor.handle_input("(", window, cx);
15516 });
15517 handle_signature_help_request(&mut cx, mocked_response).await;
15518 cx.run_until_parked();
15519 cx.editor(|editor, _, _| {
15520 assert!(
15521 editor.signature_help_state.is_shown(),
15522 "show_signature_help_after_edits should show signature help without delay"
15523 );
15524 });
15525}
15526
15527#[gpui::test]
15528async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
15529 init_test(cx, |_| {});
15530
15531 cx.update(|cx| {
15532 cx.update_global::<SettingsStore, _>(|settings, cx| {
15533 settings.update_user_settings(cx, |settings| {
15534 settings.editor.auto_signature_help = Some(false);
15535 settings.editor.show_signature_help_after_edits = Some(false);
15536 });
15537 });
15538 });
15539
15540 let mut cx = EditorLspTestContext::new_rust(
15541 lsp::ServerCapabilities {
15542 signature_help_provider: Some(lsp::SignatureHelpOptions {
15543 ..Default::default()
15544 }),
15545 ..Default::default()
15546 },
15547 cx,
15548 )
15549 .await;
15550
15551 let language = Language::new(
15552 LanguageConfig {
15553 name: "Rust".into(),
15554 brackets: BracketPairConfig {
15555 pairs: vec![
15556 BracketPair {
15557 start: "{".to_string(),
15558 end: "}".to_string(),
15559 close: true,
15560 surround: true,
15561 newline: true,
15562 },
15563 BracketPair {
15564 start: "(".to_string(),
15565 end: ")".to_string(),
15566 close: true,
15567 surround: true,
15568 newline: true,
15569 },
15570 BracketPair {
15571 start: "/*".to_string(),
15572 end: " */".to_string(),
15573 close: true,
15574 surround: true,
15575 newline: true,
15576 },
15577 BracketPair {
15578 start: "[".to_string(),
15579 end: "]".to_string(),
15580 close: false,
15581 surround: false,
15582 newline: true,
15583 },
15584 BracketPair {
15585 start: "\"".to_string(),
15586 end: "\"".to_string(),
15587 close: true,
15588 surround: true,
15589 newline: false,
15590 },
15591 BracketPair {
15592 start: "<".to_string(),
15593 end: ">".to_string(),
15594 close: false,
15595 surround: true,
15596 newline: true,
15597 },
15598 ],
15599 ..Default::default()
15600 },
15601 autoclose_before: "})]".to_string(),
15602 ..Default::default()
15603 },
15604 Some(tree_sitter_rust::LANGUAGE.into()),
15605 );
15606 let language = Arc::new(language);
15607
15608 cx.language_registry().add(language.clone());
15609 cx.update_buffer(|buffer, cx| {
15610 buffer.set_language(Some(language), cx);
15611 });
15612
15613 // Ensure that signature_help is not called when no signature help is enabled.
15614 cx.set_state(
15615 &r#"
15616 fn main() {
15617 sampleˇ
15618 }
15619 "#
15620 .unindent(),
15621 );
15622 cx.update_editor(|editor, window, cx| {
15623 editor.handle_input("(", window, cx);
15624 });
15625 cx.assert_editor_state(
15626 &"
15627 fn main() {
15628 sample(ˇ)
15629 }
15630 "
15631 .unindent(),
15632 );
15633 cx.editor(|editor, _, _| {
15634 assert!(editor.signature_help_state.task().is_none());
15635 });
15636
15637 let mocked_response = lsp::SignatureHelp {
15638 signatures: vec![lsp::SignatureInformation {
15639 label: "fn sample(param1: u8, param2: u8)".to_string(),
15640 documentation: None,
15641 parameters: Some(vec![
15642 lsp::ParameterInformation {
15643 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15644 documentation: None,
15645 },
15646 lsp::ParameterInformation {
15647 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15648 documentation: None,
15649 },
15650 ]),
15651 active_parameter: None,
15652 }],
15653 active_signature: Some(0),
15654 active_parameter: Some(0),
15655 };
15656
15657 // Ensure that signature_help is called when enabled afte edits
15658 cx.update(|_, cx| {
15659 cx.update_global::<SettingsStore, _>(|settings, cx| {
15660 settings.update_user_settings(cx, |settings| {
15661 settings.editor.auto_signature_help = Some(false);
15662 settings.editor.show_signature_help_after_edits = Some(true);
15663 });
15664 });
15665 });
15666 cx.set_state(
15667 &r#"
15668 fn main() {
15669 sampleˇ
15670 }
15671 "#
15672 .unindent(),
15673 );
15674 cx.update_editor(|editor, window, cx| {
15675 editor.handle_input("(", window, cx);
15676 });
15677 cx.assert_editor_state(
15678 &"
15679 fn main() {
15680 sample(ˇ)
15681 }
15682 "
15683 .unindent(),
15684 );
15685 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15686 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15687 .await;
15688 cx.update_editor(|editor, _, _| {
15689 let signature_help_state = editor.signature_help_state.popover().cloned();
15690 assert!(signature_help_state.is_some());
15691 let signature = signature_help_state.unwrap();
15692 assert_eq!(
15693 signature.signatures[signature.current_signature].label,
15694 "fn sample(param1: u8, param2: u8)"
15695 );
15696 editor.signature_help_state = SignatureHelpState::default();
15697 });
15698
15699 // Ensure that signature_help is called when auto signature help override is enabled
15700 cx.update(|_, cx| {
15701 cx.update_global::<SettingsStore, _>(|settings, cx| {
15702 settings.update_user_settings(cx, |settings| {
15703 settings.editor.auto_signature_help = Some(true);
15704 settings.editor.show_signature_help_after_edits = Some(false);
15705 });
15706 });
15707 });
15708 cx.set_state(
15709 &r#"
15710 fn main() {
15711 sampleˇ
15712 }
15713 "#
15714 .unindent(),
15715 );
15716 cx.update_editor(|editor, window, cx| {
15717 editor.handle_input("(", window, cx);
15718 });
15719 cx.assert_editor_state(
15720 &"
15721 fn main() {
15722 sample(ˇ)
15723 }
15724 "
15725 .unindent(),
15726 );
15727 handle_signature_help_request(&mut cx, mocked_response).await;
15728 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15729 .await;
15730 cx.editor(|editor, _, _| {
15731 let signature_help_state = editor.signature_help_state.popover().cloned();
15732 assert!(signature_help_state.is_some());
15733 let signature = signature_help_state.unwrap();
15734 assert_eq!(
15735 signature.signatures[signature.current_signature].label,
15736 "fn sample(param1: u8, param2: u8)"
15737 );
15738 });
15739}
15740
15741#[gpui::test]
15742async fn test_signature_help(cx: &mut TestAppContext) {
15743 init_test(cx, |_| {});
15744 cx.update(|cx| {
15745 cx.update_global::<SettingsStore, _>(|settings, cx| {
15746 settings.update_user_settings(cx, |settings| {
15747 settings.editor.auto_signature_help = Some(true);
15748 });
15749 });
15750 });
15751
15752 let mut cx = EditorLspTestContext::new_rust(
15753 lsp::ServerCapabilities {
15754 signature_help_provider: Some(lsp::SignatureHelpOptions {
15755 ..Default::default()
15756 }),
15757 ..Default::default()
15758 },
15759 cx,
15760 )
15761 .await;
15762
15763 // A test that directly calls `show_signature_help`
15764 cx.update_editor(|editor, window, cx| {
15765 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15766 });
15767
15768 let mocked_response = lsp::SignatureHelp {
15769 signatures: vec![lsp::SignatureInformation {
15770 label: "fn sample(param1: u8, param2: u8)".to_string(),
15771 documentation: None,
15772 parameters: Some(vec![
15773 lsp::ParameterInformation {
15774 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15775 documentation: None,
15776 },
15777 lsp::ParameterInformation {
15778 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15779 documentation: None,
15780 },
15781 ]),
15782 active_parameter: None,
15783 }],
15784 active_signature: Some(0),
15785 active_parameter: Some(0),
15786 };
15787 handle_signature_help_request(&mut cx, mocked_response).await;
15788
15789 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15790 .await;
15791
15792 cx.editor(|editor, _, _| {
15793 let signature_help_state = editor.signature_help_state.popover().cloned();
15794 assert!(signature_help_state.is_some());
15795 let signature = signature_help_state.unwrap();
15796 assert_eq!(
15797 signature.signatures[signature.current_signature].label,
15798 "fn sample(param1: u8, param2: u8)"
15799 );
15800 });
15801
15802 // When exiting outside from inside the brackets, `signature_help` is closed.
15803 cx.set_state(indoc! {"
15804 fn main() {
15805 sample(ˇ);
15806 }
15807
15808 fn sample(param1: u8, param2: u8) {}
15809 "});
15810
15811 cx.update_editor(|editor, window, cx| {
15812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15813 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
15814 });
15815 });
15816
15817 let mocked_response = lsp::SignatureHelp {
15818 signatures: Vec::new(),
15819 active_signature: None,
15820 active_parameter: None,
15821 };
15822 handle_signature_help_request(&mut cx, mocked_response).await;
15823
15824 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15825 .await;
15826
15827 cx.editor(|editor, _, _| {
15828 assert!(!editor.signature_help_state.is_shown());
15829 });
15830
15831 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
15832 cx.set_state(indoc! {"
15833 fn main() {
15834 sample(ˇ);
15835 }
15836
15837 fn sample(param1: u8, param2: u8) {}
15838 "});
15839
15840 let mocked_response = lsp::SignatureHelp {
15841 signatures: vec![lsp::SignatureInformation {
15842 label: "fn sample(param1: u8, param2: u8)".to_string(),
15843 documentation: None,
15844 parameters: Some(vec![
15845 lsp::ParameterInformation {
15846 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15847 documentation: None,
15848 },
15849 lsp::ParameterInformation {
15850 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15851 documentation: None,
15852 },
15853 ]),
15854 active_parameter: None,
15855 }],
15856 active_signature: Some(0),
15857 active_parameter: Some(0),
15858 };
15859 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15860 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15861 .await;
15862 cx.editor(|editor, _, _| {
15863 assert!(editor.signature_help_state.is_shown());
15864 });
15865
15866 // Restore the popover with more parameter input
15867 cx.set_state(indoc! {"
15868 fn main() {
15869 sample(param1, param2ˇ);
15870 }
15871
15872 fn sample(param1: u8, param2: u8) {}
15873 "});
15874
15875 let mocked_response = lsp::SignatureHelp {
15876 signatures: vec![lsp::SignatureInformation {
15877 label: "fn sample(param1: u8, param2: u8)".to_string(),
15878 documentation: None,
15879 parameters: Some(vec![
15880 lsp::ParameterInformation {
15881 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15882 documentation: None,
15883 },
15884 lsp::ParameterInformation {
15885 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15886 documentation: None,
15887 },
15888 ]),
15889 active_parameter: None,
15890 }],
15891 active_signature: Some(0),
15892 active_parameter: Some(1),
15893 };
15894 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15895 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15896 .await;
15897
15898 // When selecting a range, the popover is gone.
15899 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
15900 cx.update_editor(|editor, window, cx| {
15901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15902 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15903 })
15904 });
15905 cx.assert_editor_state(indoc! {"
15906 fn main() {
15907 sample(param1, «ˇparam2»);
15908 }
15909
15910 fn sample(param1: u8, param2: u8) {}
15911 "});
15912 cx.editor(|editor, _, _| {
15913 assert!(!editor.signature_help_state.is_shown());
15914 });
15915
15916 // When unselecting again, the popover is back if within the brackets.
15917 cx.update_editor(|editor, window, cx| {
15918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15919 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15920 })
15921 });
15922 cx.assert_editor_state(indoc! {"
15923 fn main() {
15924 sample(param1, ˇparam2);
15925 }
15926
15927 fn sample(param1: u8, param2: u8) {}
15928 "});
15929 handle_signature_help_request(&mut cx, mocked_response).await;
15930 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15931 .await;
15932 cx.editor(|editor, _, _| {
15933 assert!(editor.signature_help_state.is_shown());
15934 });
15935
15936 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
15937 cx.update_editor(|editor, window, cx| {
15938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15939 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
15940 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15941 })
15942 });
15943 cx.assert_editor_state(indoc! {"
15944 fn main() {
15945 sample(param1, ˇparam2);
15946 }
15947
15948 fn sample(param1: u8, param2: u8) {}
15949 "});
15950
15951 let mocked_response = lsp::SignatureHelp {
15952 signatures: vec![lsp::SignatureInformation {
15953 label: "fn sample(param1: u8, param2: u8)".to_string(),
15954 documentation: None,
15955 parameters: Some(vec![
15956 lsp::ParameterInformation {
15957 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15958 documentation: None,
15959 },
15960 lsp::ParameterInformation {
15961 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15962 documentation: None,
15963 },
15964 ]),
15965 active_parameter: None,
15966 }],
15967 active_signature: Some(0),
15968 active_parameter: Some(1),
15969 };
15970 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15971 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15972 .await;
15973 cx.update_editor(|editor, _, cx| {
15974 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
15975 });
15976 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15977 .await;
15978 cx.update_editor(|editor, window, cx| {
15979 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15980 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15981 })
15982 });
15983 cx.assert_editor_state(indoc! {"
15984 fn main() {
15985 sample(param1, «ˇparam2»);
15986 }
15987
15988 fn sample(param1: u8, param2: u8) {}
15989 "});
15990 cx.update_editor(|editor, window, cx| {
15991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15992 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15993 })
15994 });
15995 cx.assert_editor_state(indoc! {"
15996 fn main() {
15997 sample(param1, ˇparam2);
15998 }
15999
16000 fn sample(param1: u8, param2: u8) {}
16001 "});
16002 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
16003 .await;
16004}
16005
16006#[gpui::test]
16007async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
16008 init_test(cx, |_| {});
16009
16010 let mut cx = EditorLspTestContext::new_rust(
16011 lsp::ServerCapabilities {
16012 signature_help_provider: Some(lsp::SignatureHelpOptions {
16013 ..Default::default()
16014 }),
16015 ..Default::default()
16016 },
16017 cx,
16018 )
16019 .await;
16020
16021 cx.set_state(indoc! {"
16022 fn main() {
16023 overloadedˇ
16024 }
16025 "});
16026
16027 cx.update_editor(|editor, window, cx| {
16028 editor.handle_input("(", window, cx);
16029 editor.show_signature_help(&ShowSignatureHelp, window, cx);
16030 });
16031
16032 // Mock response with 3 signatures
16033 let mocked_response = lsp::SignatureHelp {
16034 signatures: vec![
16035 lsp::SignatureInformation {
16036 label: "fn overloaded(x: i32)".to_string(),
16037 documentation: None,
16038 parameters: Some(vec![lsp::ParameterInformation {
16039 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
16040 documentation: None,
16041 }]),
16042 active_parameter: None,
16043 },
16044 lsp::SignatureInformation {
16045 label: "fn overloaded(x: i32, y: i32)".to_string(),
16046 documentation: None,
16047 parameters: Some(vec![
16048 lsp::ParameterInformation {
16049 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
16050 documentation: None,
16051 },
16052 lsp::ParameterInformation {
16053 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
16054 documentation: None,
16055 },
16056 ]),
16057 active_parameter: None,
16058 },
16059 lsp::SignatureInformation {
16060 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
16061 documentation: None,
16062 parameters: Some(vec![
16063 lsp::ParameterInformation {
16064 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
16065 documentation: None,
16066 },
16067 lsp::ParameterInformation {
16068 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
16069 documentation: None,
16070 },
16071 lsp::ParameterInformation {
16072 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
16073 documentation: None,
16074 },
16075 ]),
16076 active_parameter: None,
16077 },
16078 ],
16079 active_signature: Some(1),
16080 active_parameter: Some(0),
16081 };
16082 handle_signature_help_request(&mut cx, mocked_response).await;
16083
16084 cx.condition(|editor, _| editor.signature_help_state.is_shown())
16085 .await;
16086
16087 // Verify we have multiple signatures and the right one is selected
16088 cx.editor(|editor, _, _| {
16089 let popover = editor.signature_help_state.popover().cloned().unwrap();
16090 assert_eq!(popover.signatures.len(), 3);
16091 // active_signature was 1, so that should be the current
16092 assert_eq!(popover.current_signature, 1);
16093 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
16094 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
16095 assert_eq!(
16096 popover.signatures[2].label,
16097 "fn overloaded(x: i32, y: i32, z: i32)"
16098 );
16099 });
16100
16101 // Test navigation functionality
16102 cx.update_editor(|editor, window, cx| {
16103 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
16104 });
16105
16106 cx.editor(|editor, _, _| {
16107 let popover = editor.signature_help_state.popover().cloned().unwrap();
16108 assert_eq!(popover.current_signature, 2);
16109 });
16110
16111 // Test wrap around
16112 cx.update_editor(|editor, window, cx| {
16113 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
16114 });
16115
16116 cx.editor(|editor, _, _| {
16117 let popover = editor.signature_help_state.popover().cloned().unwrap();
16118 assert_eq!(popover.current_signature, 0);
16119 });
16120
16121 // Test previous navigation
16122 cx.update_editor(|editor, window, cx| {
16123 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
16124 });
16125
16126 cx.editor(|editor, _, _| {
16127 let popover = editor.signature_help_state.popover().cloned().unwrap();
16128 assert_eq!(popover.current_signature, 2);
16129 });
16130}
16131
16132#[gpui::test]
16133async fn test_completion_mode(cx: &mut TestAppContext) {
16134 init_test(cx, |_| {});
16135 let mut cx = EditorLspTestContext::new_rust(
16136 lsp::ServerCapabilities {
16137 completion_provider: Some(lsp::CompletionOptions {
16138 resolve_provider: Some(true),
16139 ..Default::default()
16140 }),
16141 ..Default::default()
16142 },
16143 cx,
16144 )
16145 .await;
16146
16147 struct Run {
16148 run_description: &'static str,
16149 initial_state: String,
16150 buffer_marked_text: String,
16151 completion_label: &'static str,
16152 completion_text: &'static str,
16153 expected_with_insert_mode: String,
16154 expected_with_replace_mode: String,
16155 expected_with_replace_subsequence_mode: String,
16156 expected_with_replace_suffix_mode: String,
16157 }
16158
16159 let runs = [
16160 Run {
16161 run_description: "Start of word matches completion text",
16162 initial_state: "before ediˇ after".into(),
16163 buffer_marked_text: "before <edi|> after".into(),
16164 completion_label: "editor",
16165 completion_text: "editor",
16166 expected_with_insert_mode: "before editorˇ after".into(),
16167 expected_with_replace_mode: "before editorˇ after".into(),
16168 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
16169 expected_with_replace_suffix_mode: "before editorˇ after".into(),
16170 },
16171 Run {
16172 run_description: "Accept same text at the middle of the word",
16173 initial_state: "before ediˇtor after".into(),
16174 buffer_marked_text: "before <edi|tor> after".into(),
16175 completion_label: "editor",
16176 completion_text: "editor",
16177 expected_with_insert_mode: "before editorˇtor after".into(),
16178 expected_with_replace_mode: "before editorˇ after".into(),
16179 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
16180 expected_with_replace_suffix_mode: "before editorˇ after".into(),
16181 },
16182 Run {
16183 run_description: "End of word matches completion text -- cursor at end",
16184 initial_state: "before torˇ after".into(),
16185 buffer_marked_text: "before <tor|> after".into(),
16186 completion_label: "editor",
16187 completion_text: "editor",
16188 expected_with_insert_mode: "before editorˇ after".into(),
16189 expected_with_replace_mode: "before editorˇ after".into(),
16190 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
16191 expected_with_replace_suffix_mode: "before editorˇ after".into(),
16192 },
16193 Run {
16194 run_description: "End of word matches completion text -- cursor at start",
16195 initial_state: "before ˇtor after".into(),
16196 buffer_marked_text: "before <|tor> after".into(),
16197 completion_label: "editor",
16198 completion_text: "editor",
16199 expected_with_insert_mode: "before editorˇtor after".into(),
16200 expected_with_replace_mode: "before editorˇ after".into(),
16201 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
16202 expected_with_replace_suffix_mode: "before editorˇ after".into(),
16203 },
16204 Run {
16205 run_description: "Prepend text containing whitespace",
16206 initial_state: "pˇfield: bool".into(),
16207 buffer_marked_text: "<p|field>: bool".into(),
16208 completion_label: "pub ",
16209 completion_text: "pub ",
16210 expected_with_insert_mode: "pub ˇfield: bool".into(),
16211 expected_with_replace_mode: "pub ˇ: bool".into(),
16212 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
16213 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
16214 },
16215 Run {
16216 run_description: "Add element to start of list",
16217 initial_state: "[element_ˇelement_2]".into(),
16218 buffer_marked_text: "[<element_|element_2>]".into(),
16219 completion_label: "element_1",
16220 completion_text: "element_1",
16221 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
16222 expected_with_replace_mode: "[element_1ˇ]".into(),
16223 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
16224 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
16225 },
16226 Run {
16227 run_description: "Add element to start of list -- first and second elements are equal",
16228 initial_state: "[elˇelement]".into(),
16229 buffer_marked_text: "[<el|element>]".into(),
16230 completion_label: "element",
16231 completion_text: "element",
16232 expected_with_insert_mode: "[elementˇelement]".into(),
16233 expected_with_replace_mode: "[elementˇ]".into(),
16234 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
16235 expected_with_replace_suffix_mode: "[elementˇ]".into(),
16236 },
16237 Run {
16238 run_description: "Ends with matching suffix",
16239 initial_state: "SubˇError".into(),
16240 buffer_marked_text: "<Sub|Error>".into(),
16241 completion_label: "SubscriptionError",
16242 completion_text: "SubscriptionError",
16243 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
16244 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
16245 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
16246 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
16247 },
16248 Run {
16249 run_description: "Suffix is a subsequence -- contiguous",
16250 initial_state: "SubˇErr".into(),
16251 buffer_marked_text: "<Sub|Err>".into(),
16252 completion_label: "SubscriptionError",
16253 completion_text: "SubscriptionError",
16254 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
16255 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
16256 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
16257 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
16258 },
16259 Run {
16260 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
16261 initial_state: "Suˇscrirr".into(),
16262 buffer_marked_text: "<Su|scrirr>".into(),
16263 completion_label: "SubscriptionError",
16264 completion_text: "SubscriptionError",
16265 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
16266 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
16267 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
16268 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
16269 },
16270 Run {
16271 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
16272 initial_state: "foo(indˇix)".into(),
16273 buffer_marked_text: "foo(<ind|ix>)".into(),
16274 completion_label: "node_index",
16275 completion_text: "node_index",
16276 expected_with_insert_mode: "foo(node_indexˇix)".into(),
16277 expected_with_replace_mode: "foo(node_indexˇ)".into(),
16278 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
16279 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
16280 },
16281 Run {
16282 run_description: "Replace range ends before cursor - should extend to cursor",
16283 initial_state: "before editˇo after".into(),
16284 buffer_marked_text: "before <{ed}>it|o after".into(),
16285 completion_label: "editor",
16286 completion_text: "editor",
16287 expected_with_insert_mode: "before editorˇo after".into(),
16288 expected_with_replace_mode: "before editorˇo after".into(),
16289 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
16290 expected_with_replace_suffix_mode: "before editorˇo after".into(),
16291 },
16292 Run {
16293 run_description: "Uses label for suffix matching",
16294 initial_state: "before ediˇtor after".into(),
16295 buffer_marked_text: "before <edi|tor> after".into(),
16296 completion_label: "editor",
16297 completion_text: "editor()",
16298 expected_with_insert_mode: "before editor()ˇtor after".into(),
16299 expected_with_replace_mode: "before editor()ˇ after".into(),
16300 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
16301 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
16302 },
16303 Run {
16304 run_description: "Case insensitive subsequence and suffix matching",
16305 initial_state: "before EDiˇtoR after".into(),
16306 buffer_marked_text: "before <EDi|toR> after".into(),
16307 completion_label: "editor",
16308 completion_text: "editor",
16309 expected_with_insert_mode: "before editorˇtoR after".into(),
16310 expected_with_replace_mode: "before editorˇ after".into(),
16311 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
16312 expected_with_replace_suffix_mode: "before editorˇ after".into(),
16313 },
16314 ];
16315
16316 for run in runs {
16317 let run_variations = [
16318 (LspInsertMode::Insert, run.expected_with_insert_mode),
16319 (LspInsertMode::Replace, run.expected_with_replace_mode),
16320 (
16321 LspInsertMode::ReplaceSubsequence,
16322 run.expected_with_replace_subsequence_mode,
16323 ),
16324 (
16325 LspInsertMode::ReplaceSuffix,
16326 run.expected_with_replace_suffix_mode,
16327 ),
16328 ];
16329
16330 for (lsp_insert_mode, expected_text) in run_variations {
16331 eprintln!(
16332 "run = {:?}, mode = {lsp_insert_mode:.?}",
16333 run.run_description,
16334 );
16335
16336 update_test_language_settings(&mut cx, &|settings| {
16337 settings.defaults.completions = Some(CompletionSettingsContent {
16338 lsp_insert_mode: Some(lsp_insert_mode),
16339 words: Some(WordsCompletionMode::Disabled),
16340 words_min_length: Some(0),
16341 ..Default::default()
16342 });
16343 });
16344
16345 cx.set_state(&run.initial_state);
16346
16347 // Set up resolve handler before showing completions, since resolve may be
16348 // triggered when menu becomes visible (for documentation), not just on confirm.
16349 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
16350 move |_, _, _| async move {
16351 Ok(lsp::CompletionItem {
16352 additional_text_edits: None,
16353 ..Default::default()
16354 })
16355 },
16356 );
16357
16358 cx.update_editor(|editor, window, cx| {
16359 editor.show_completions(&ShowCompletions, window, cx);
16360 });
16361
16362 let counter = Arc::new(AtomicUsize::new(0));
16363 handle_completion_request_with_insert_and_replace(
16364 &mut cx,
16365 &run.buffer_marked_text,
16366 vec![(run.completion_label, run.completion_text)],
16367 counter.clone(),
16368 )
16369 .await;
16370 cx.condition(|editor, _| editor.context_menu_visible())
16371 .await;
16372 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16373
16374 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16375 editor
16376 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16377 .unwrap()
16378 });
16379 cx.assert_editor_state(&expected_text);
16380 apply_additional_edits.await.unwrap();
16381 }
16382 }
16383}
16384
16385#[gpui::test]
16386async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
16387 init_test(cx, |_| {});
16388 let mut cx = EditorLspTestContext::new_rust(
16389 lsp::ServerCapabilities {
16390 completion_provider: Some(lsp::CompletionOptions {
16391 resolve_provider: Some(true),
16392 ..Default::default()
16393 }),
16394 ..Default::default()
16395 },
16396 cx,
16397 )
16398 .await;
16399
16400 let initial_state = "SubˇError";
16401 let buffer_marked_text = "<Sub|Error>";
16402 let completion_text = "SubscriptionError";
16403 let expected_with_insert_mode = "SubscriptionErrorˇError";
16404 let expected_with_replace_mode = "SubscriptionErrorˇ";
16405
16406 update_test_language_settings(&mut cx, &|settings| {
16407 settings.defaults.completions = Some(CompletionSettingsContent {
16408 words: Some(WordsCompletionMode::Disabled),
16409 words_min_length: Some(0),
16410 // set the opposite here to ensure that the action is overriding the default behavior
16411 lsp_insert_mode: Some(LspInsertMode::Insert),
16412 ..Default::default()
16413 });
16414 });
16415
16416 cx.set_state(initial_state);
16417 cx.update_editor(|editor, window, cx| {
16418 editor.show_completions(&ShowCompletions, window, cx);
16419 });
16420
16421 let counter = Arc::new(AtomicUsize::new(0));
16422 handle_completion_request_with_insert_and_replace(
16423 &mut cx,
16424 buffer_marked_text,
16425 vec![(completion_text, completion_text)],
16426 counter.clone(),
16427 )
16428 .await;
16429 cx.condition(|editor, _| editor.context_menu_visible())
16430 .await;
16431 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16432
16433 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16434 editor
16435 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16436 .unwrap()
16437 });
16438 cx.assert_editor_state(expected_with_replace_mode);
16439 handle_resolve_completion_request(&mut cx, None).await;
16440 apply_additional_edits.await.unwrap();
16441
16442 update_test_language_settings(&mut cx, &|settings| {
16443 settings.defaults.completions = Some(CompletionSettingsContent {
16444 words: Some(WordsCompletionMode::Disabled),
16445 words_min_length: Some(0),
16446 // set the opposite here to ensure that the action is overriding the default behavior
16447 lsp_insert_mode: Some(LspInsertMode::Replace),
16448 ..Default::default()
16449 });
16450 });
16451
16452 cx.set_state(initial_state);
16453 cx.update_editor(|editor, window, cx| {
16454 editor.show_completions(&ShowCompletions, window, cx);
16455 });
16456 handle_completion_request_with_insert_and_replace(
16457 &mut cx,
16458 buffer_marked_text,
16459 vec![(completion_text, completion_text)],
16460 counter.clone(),
16461 )
16462 .await;
16463 cx.condition(|editor, _| editor.context_menu_visible())
16464 .await;
16465 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16466
16467 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16468 editor
16469 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
16470 .unwrap()
16471 });
16472 cx.assert_editor_state(expected_with_insert_mode);
16473 handle_resolve_completion_request(&mut cx, None).await;
16474 apply_additional_edits.await.unwrap();
16475}
16476
16477#[gpui::test]
16478async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
16479 init_test(cx, |_| {});
16480 let mut cx = EditorLspTestContext::new_rust(
16481 lsp::ServerCapabilities {
16482 completion_provider: Some(lsp::CompletionOptions {
16483 resolve_provider: Some(true),
16484 ..Default::default()
16485 }),
16486 ..Default::default()
16487 },
16488 cx,
16489 )
16490 .await;
16491
16492 // scenario: surrounding text matches completion text
16493 let completion_text = "to_offset";
16494 let initial_state = indoc! {"
16495 1. buf.to_offˇsuffix
16496 2. buf.to_offˇsuf
16497 3. buf.to_offˇfix
16498 4. buf.to_offˇ
16499 5. into_offˇensive
16500 6. ˇsuffix
16501 7. let ˇ //
16502 8. aaˇzz
16503 9. buf.to_off«zzzzzˇ»suffix
16504 10. buf.«ˇzzzzz»suffix
16505 11. to_off«ˇzzzzz»
16506
16507 buf.to_offˇsuffix // newest cursor
16508 "};
16509 let completion_marked_buffer = indoc! {"
16510 1. buf.to_offsuffix
16511 2. buf.to_offsuf
16512 3. buf.to_offfix
16513 4. buf.to_off
16514 5. into_offensive
16515 6. suffix
16516 7. let //
16517 8. aazz
16518 9. buf.to_offzzzzzsuffix
16519 10. buf.zzzzzsuffix
16520 11. to_offzzzzz
16521
16522 buf.<to_off|suffix> // newest cursor
16523 "};
16524 let expected = indoc! {"
16525 1. buf.to_offsetˇ
16526 2. buf.to_offsetˇsuf
16527 3. buf.to_offsetˇfix
16528 4. buf.to_offsetˇ
16529 5. into_offsetˇensive
16530 6. to_offsetˇsuffix
16531 7. let to_offsetˇ //
16532 8. aato_offsetˇzz
16533 9. buf.to_offsetˇ
16534 10. buf.to_offsetˇsuffix
16535 11. to_offsetˇ
16536
16537 buf.to_offsetˇ // newest cursor
16538 "};
16539 cx.set_state(initial_state);
16540 cx.update_editor(|editor, window, cx| {
16541 editor.show_completions(&ShowCompletions, window, cx);
16542 });
16543 handle_completion_request_with_insert_and_replace(
16544 &mut cx,
16545 completion_marked_buffer,
16546 vec![(completion_text, completion_text)],
16547 Arc::new(AtomicUsize::new(0)),
16548 )
16549 .await;
16550 cx.condition(|editor, _| editor.context_menu_visible())
16551 .await;
16552 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16553 editor
16554 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16555 .unwrap()
16556 });
16557 cx.assert_editor_state(expected);
16558 handle_resolve_completion_request(&mut cx, None).await;
16559 apply_additional_edits.await.unwrap();
16560
16561 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
16562 let completion_text = "foo_and_bar";
16563 let initial_state = indoc! {"
16564 1. ooanbˇ
16565 2. zooanbˇ
16566 3. ooanbˇz
16567 4. zooanbˇz
16568 5. ooanˇ
16569 6. oanbˇ
16570
16571 ooanbˇ
16572 "};
16573 let completion_marked_buffer = indoc! {"
16574 1. ooanb
16575 2. zooanb
16576 3. ooanbz
16577 4. zooanbz
16578 5. ooan
16579 6. oanb
16580
16581 <ooanb|>
16582 "};
16583 let expected = indoc! {"
16584 1. foo_and_barˇ
16585 2. zfoo_and_barˇ
16586 3. foo_and_barˇz
16587 4. zfoo_and_barˇz
16588 5. ooanfoo_and_barˇ
16589 6. oanbfoo_and_barˇ
16590
16591 foo_and_barˇ
16592 "};
16593 cx.set_state(initial_state);
16594 cx.update_editor(|editor, window, cx| {
16595 editor.show_completions(&ShowCompletions, window, cx);
16596 });
16597 handle_completion_request_with_insert_and_replace(
16598 &mut cx,
16599 completion_marked_buffer,
16600 vec![(completion_text, completion_text)],
16601 Arc::new(AtomicUsize::new(0)),
16602 )
16603 .await;
16604 cx.condition(|editor, _| editor.context_menu_visible())
16605 .await;
16606 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16607 editor
16608 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16609 .unwrap()
16610 });
16611 cx.assert_editor_state(expected);
16612 handle_resolve_completion_request(&mut cx, None).await;
16613 apply_additional_edits.await.unwrap();
16614
16615 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
16616 // (expects the same as if it was inserted at the end)
16617 let completion_text = "foo_and_bar";
16618 let initial_state = indoc! {"
16619 1. ooˇanb
16620 2. zooˇanb
16621 3. ooˇanbz
16622 4. zooˇanbz
16623
16624 ooˇanb
16625 "};
16626 let completion_marked_buffer = indoc! {"
16627 1. ooanb
16628 2. zooanb
16629 3. ooanbz
16630 4. zooanbz
16631
16632 <oo|anb>
16633 "};
16634 let expected = indoc! {"
16635 1. foo_and_barˇ
16636 2. zfoo_and_barˇ
16637 3. foo_and_barˇz
16638 4. zfoo_and_barˇz
16639
16640 foo_and_barˇ
16641 "};
16642 cx.set_state(initial_state);
16643 cx.update_editor(|editor, window, cx| {
16644 editor.show_completions(&ShowCompletions, window, cx);
16645 });
16646 handle_completion_request_with_insert_and_replace(
16647 &mut cx,
16648 completion_marked_buffer,
16649 vec![(completion_text, completion_text)],
16650 Arc::new(AtomicUsize::new(0)),
16651 )
16652 .await;
16653 cx.condition(|editor, _| editor.context_menu_visible())
16654 .await;
16655 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16656 editor
16657 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16658 .unwrap()
16659 });
16660 cx.assert_editor_state(expected);
16661 handle_resolve_completion_request(&mut cx, None).await;
16662 apply_additional_edits.await.unwrap();
16663}
16664
16665// This used to crash
16666#[gpui::test]
16667async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
16668 init_test(cx, |_| {});
16669
16670 let buffer_text = indoc! {"
16671 fn main() {
16672 10.satu;
16673
16674 //
16675 // separate1
16676 // separate2
16677 // separate3
16678 //
16679
16680 10.satu20;
16681 }
16682 "};
16683 let multibuffer_text_with_selections = indoc! {"
16684 fn main() {
16685 10.satuˇ;
16686
16687 //
16688
16689 10.satuˇ20;
16690 }
16691 "};
16692 let expected_multibuffer = indoc! {"
16693 fn main() {
16694 10.saturating_sub()ˇ;
16695
16696 //
16697
16698 10.saturating_sub()ˇ;
16699 }
16700 "};
16701
16702 let fs = FakeFs::new(cx.executor());
16703 fs.insert_tree(
16704 path!("/a"),
16705 json!({
16706 "main.rs": buffer_text,
16707 }),
16708 )
16709 .await;
16710
16711 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16712 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16713 language_registry.add(rust_lang());
16714 let mut fake_servers = language_registry.register_fake_lsp(
16715 "Rust",
16716 FakeLspAdapter {
16717 capabilities: lsp::ServerCapabilities {
16718 completion_provider: Some(lsp::CompletionOptions {
16719 resolve_provider: None,
16720 ..lsp::CompletionOptions::default()
16721 }),
16722 ..lsp::ServerCapabilities::default()
16723 },
16724 ..FakeLspAdapter::default()
16725 },
16726 );
16727 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16728 let workspace = window
16729 .read_with(cx, |mw, _| mw.workspace().clone())
16730 .unwrap();
16731 let cx = &mut VisualTestContext::from_window(*window, cx);
16732 let buffer = project
16733 .update(cx, |project, cx| {
16734 project.open_local_buffer(path!("/a/main.rs"), cx)
16735 })
16736 .await
16737 .unwrap();
16738
16739 let multi_buffer = cx.new(|cx| {
16740 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
16741 multi_buffer.set_excerpts_for_path(
16742 PathKey::sorted(0),
16743 buffer.clone(),
16744 [
16745 Point::zero()..Point::new(2, 0),
16746 Point::new(7, 0)..buffer.read(cx).max_point(),
16747 ],
16748 0,
16749 cx,
16750 );
16751 multi_buffer
16752 });
16753
16754 let editor = workspace.update_in(cx, |_, window, cx| {
16755 cx.new(|cx| {
16756 Editor::new(
16757 EditorMode::Full {
16758 scale_ui_elements_with_buffer_font_size: false,
16759 show_active_line_background: false,
16760 sizing_behavior: SizingBehavior::Default,
16761 },
16762 multi_buffer.clone(),
16763 Some(project.clone()),
16764 window,
16765 cx,
16766 )
16767 })
16768 });
16769
16770 let pane = workspace.update_in(cx, |workspace, _, _| workspace.active_pane().clone());
16771 pane.update_in(cx, |pane, window, cx| {
16772 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
16773 });
16774
16775 let fake_server = fake_servers.next().await.unwrap();
16776 cx.run_until_parked();
16777
16778 editor.update_in(cx, |editor, window, cx| {
16779 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16780 s.select_ranges([
16781 Point::new(1, 11)..Point::new(1, 11),
16782 Point::new(5, 11)..Point::new(5, 11),
16783 ])
16784 });
16785
16786 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
16787 });
16788
16789 editor.update_in(cx, |editor, window, cx| {
16790 editor.show_completions(&ShowCompletions, window, cx);
16791 });
16792
16793 fake_server
16794 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16795 let completion_item = lsp::CompletionItem {
16796 label: "saturating_sub()".into(),
16797 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16798 lsp::InsertReplaceEdit {
16799 new_text: "saturating_sub()".to_owned(),
16800 insert: lsp::Range::new(
16801 lsp::Position::new(9, 7),
16802 lsp::Position::new(9, 11),
16803 ),
16804 replace: lsp::Range::new(
16805 lsp::Position::new(9, 7),
16806 lsp::Position::new(9, 13),
16807 ),
16808 },
16809 )),
16810 ..lsp::CompletionItem::default()
16811 };
16812
16813 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
16814 })
16815 .next()
16816 .await
16817 .unwrap();
16818
16819 cx.condition(&editor, |editor, _| editor.context_menu_visible())
16820 .await;
16821
16822 editor
16823 .update_in(cx, |editor, window, cx| {
16824 editor
16825 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16826 .unwrap()
16827 })
16828 .await
16829 .unwrap();
16830
16831 editor.update(cx, |editor, cx| {
16832 assert_text_with_selections(editor, expected_multibuffer, cx);
16833 })
16834}
16835
16836#[gpui::test]
16837async fn test_completion(cx: &mut TestAppContext) {
16838 init_test(cx, |_| {});
16839
16840 let mut cx = EditorLspTestContext::new_rust(
16841 lsp::ServerCapabilities {
16842 completion_provider: Some(lsp::CompletionOptions {
16843 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16844 resolve_provider: Some(true),
16845 ..Default::default()
16846 }),
16847 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16848 ..Default::default()
16849 },
16850 cx,
16851 )
16852 .await;
16853 let counter = Arc::new(AtomicUsize::new(0));
16854
16855 cx.set_state(indoc! {"
16856 oneˇ
16857 two
16858 three
16859 "});
16860 cx.simulate_keystroke(".");
16861 handle_completion_request(
16862 indoc! {"
16863 one.|<>
16864 two
16865 three
16866 "},
16867 vec!["first_completion", "second_completion"],
16868 true,
16869 counter.clone(),
16870 &mut cx,
16871 )
16872 .await;
16873 cx.condition(|editor, _| editor.context_menu_visible())
16874 .await;
16875 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16876
16877 let _handler = handle_signature_help_request(
16878 &mut cx,
16879 lsp::SignatureHelp {
16880 signatures: vec![lsp::SignatureInformation {
16881 label: "test signature".to_string(),
16882 documentation: None,
16883 parameters: Some(vec![lsp::ParameterInformation {
16884 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
16885 documentation: None,
16886 }]),
16887 active_parameter: None,
16888 }],
16889 active_signature: None,
16890 active_parameter: None,
16891 },
16892 );
16893 cx.update_editor(|editor, window, cx| {
16894 assert!(
16895 !editor.signature_help_state.is_shown(),
16896 "No signature help was called for"
16897 );
16898 editor.show_signature_help(&ShowSignatureHelp, window, cx);
16899 });
16900 cx.run_until_parked();
16901 cx.update_editor(|editor, _, _| {
16902 assert!(
16903 !editor.signature_help_state.is_shown(),
16904 "No signature help should be shown when completions menu is open"
16905 );
16906 });
16907
16908 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16909 editor.context_menu_next(&Default::default(), window, cx);
16910 editor
16911 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16912 .unwrap()
16913 });
16914 cx.assert_editor_state(indoc! {"
16915 one.second_completionˇ
16916 two
16917 three
16918 "});
16919
16920 handle_resolve_completion_request(
16921 &mut cx,
16922 Some(vec![
16923 (
16924 //This overlaps with the primary completion edit which is
16925 //misbehavior from the LSP spec, test that we filter it out
16926 indoc! {"
16927 one.second_ˇcompletion
16928 two
16929 threeˇ
16930 "},
16931 "overlapping additional edit",
16932 ),
16933 (
16934 indoc! {"
16935 one.second_completion
16936 two
16937 threeˇ
16938 "},
16939 "\nadditional edit",
16940 ),
16941 ]),
16942 )
16943 .await;
16944 apply_additional_edits.await.unwrap();
16945 cx.assert_editor_state(indoc! {"
16946 one.second_completionˇ
16947 two
16948 three
16949 additional edit
16950 "});
16951
16952 cx.set_state(indoc! {"
16953 one.second_completion
16954 twoˇ
16955 threeˇ
16956 additional edit
16957 "});
16958 cx.simulate_keystroke(" ");
16959 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16960 cx.simulate_keystroke("s");
16961 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16962
16963 cx.assert_editor_state(indoc! {"
16964 one.second_completion
16965 two sˇ
16966 three sˇ
16967 additional edit
16968 "});
16969 handle_completion_request(
16970 indoc! {"
16971 one.second_completion
16972 two s
16973 three <s|>
16974 additional edit
16975 "},
16976 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16977 true,
16978 counter.clone(),
16979 &mut cx,
16980 )
16981 .await;
16982 cx.condition(|editor, _| editor.context_menu_visible())
16983 .await;
16984 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16985
16986 cx.simulate_keystroke("i");
16987
16988 handle_completion_request(
16989 indoc! {"
16990 one.second_completion
16991 two si
16992 three <si|>
16993 additional edit
16994 "},
16995 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16996 true,
16997 counter.clone(),
16998 &mut cx,
16999 )
17000 .await;
17001 cx.condition(|editor, _| editor.context_menu_visible())
17002 .await;
17003 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
17004
17005 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17006 editor
17007 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17008 .unwrap()
17009 });
17010 cx.assert_editor_state(indoc! {"
17011 one.second_completion
17012 two sixth_completionˇ
17013 three sixth_completionˇ
17014 additional edit
17015 "});
17016
17017 apply_additional_edits.await.unwrap();
17018
17019 update_test_language_settings(&mut cx, &|settings| {
17020 settings.defaults.show_completions_on_input = Some(false);
17021 });
17022 cx.set_state("editorˇ");
17023 cx.simulate_keystroke(".");
17024 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
17025 cx.simulate_keystrokes("c l o");
17026 cx.assert_editor_state("editor.cloˇ");
17027 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
17028 cx.update_editor(|editor, window, cx| {
17029 editor.show_completions(&ShowCompletions, window, cx);
17030 });
17031 handle_completion_request(
17032 "editor.<clo|>",
17033 vec!["close", "clobber"],
17034 true,
17035 counter.clone(),
17036 &mut cx,
17037 )
17038 .await;
17039 cx.condition(|editor, _| editor.context_menu_visible())
17040 .await;
17041 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
17042
17043 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17044 editor
17045 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17046 .unwrap()
17047 });
17048 cx.assert_editor_state("editor.clobberˇ");
17049 handle_resolve_completion_request(&mut cx, None).await;
17050 apply_additional_edits.await.unwrap();
17051}
17052
17053#[gpui::test]
17054async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
17055 init_test(cx, |_| {});
17056
17057 let fs = FakeFs::new(cx.executor());
17058 fs.insert_tree(
17059 path!("/a"),
17060 json!({
17061 "main.rs": "",
17062 }),
17063 )
17064 .await;
17065
17066 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17067 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17068 language_registry.add(rust_lang());
17069 let command_calls = Arc::new(AtomicUsize::new(0));
17070 let registered_command = "_the/command";
17071
17072 let closure_command_calls = command_calls.clone();
17073 let mut fake_servers = language_registry.register_fake_lsp(
17074 "Rust",
17075 FakeLspAdapter {
17076 capabilities: lsp::ServerCapabilities {
17077 completion_provider: Some(lsp::CompletionOptions {
17078 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17079 ..lsp::CompletionOptions::default()
17080 }),
17081 execute_command_provider: Some(lsp::ExecuteCommandOptions {
17082 commands: vec![registered_command.to_owned()],
17083 ..lsp::ExecuteCommandOptions::default()
17084 }),
17085 ..lsp::ServerCapabilities::default()
17086 },
17087 initializer: Some(Box::new(move |fake_server| {
17088 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
17089 move |params, _| async move {
17090 Ok(Some(lsp::CompletionResponse::Array(vec![
17091 lsp::CompletionItem {
17092 label: "registered_command".to_owned(),
17093 text_edit: gen_text_edit(¶ms, ""),
17094 command: Some(lsp::Command {
17095 title: registered_command.to_owned(),
17096 command: "_the/command".to_owned(),
17097 arguments: Some(vec![serde_json::Value::Bool(true)]),
17098 }),
17099 ..lsp::CompletionItem::default()
17100 },
17101 lsp::CompletionItem {
17102 label: "unregistered_command".to_owned(),
17103 text_edit: gen_text_edit(¶ms, ""),
17104 command: Some(lsp::Command {
17105 title: "????????????".to_owned(),
17106 command: "????????????".to_owned(),
17107 arguments: Some(vec![serde_json::Value::Null]),
17108 }),
17109 ..lsp::CompletionItem::default()
17110 },
17111 ])))
17112 },
17113 );
17114 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
17115 let command_calls = closure_command_calls.clone();
17116 move |params, _| {
17117 assert_eq!(params.command, registered_command);
17118 let command_calls = command_calls.clone();
17119 async move {
17120 command_calls.fetch_add(1, atomic::Ordering::Release);
17121 Ok(Some(json!(null)))
17122 }
17123 }
17124 });
17125 })),
17126 ..FakeLspAdapter::default()
17127 },
17128 );
17129 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
17130 let workspace = window
17131 .read_with(cx, |mw, _| mw.workspace().clone())
17132 .unwrap();
17133 let cx = &mut VisualTestContext::from_window(*window, cx);
17134 let editor = workspace
17135 .update_in(cx, |workspace, window, cx| {
17136 workspace.open_abs_path(
17137 PathBuf::from(path!("/a/main.rs")),
17138 OpenOptions::default(),
17139 window,
17140 cx,
17141 )
17142 })
17143 .await
17144 .unwrap()
17145 .downcast::<Editor>()
17146 .unwrap();
17147 let _fake_server = fake_servers.next().await.unwrap();
17148 cx.run_until_parked();
17149
17150 editor.update_in(cx, |editor, window, cx| {
17151 cx.focus_self(window);
17152 editor.move_to_end(&MoveToEnd, window, cx);
17153 editor.handle_input(".", window, cx);
17154 });
17155 cx.run_until_parked();
17156 editor.update(cx, |editor, _| {
17157 assert!(editor.context_menu_visible());
17158 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17159 {
17160 let completion_labels = menu
17161 .completions
17162 .borrow()
17163 .iter()
17164 .map(|c| c.label.text.clone())
17165 .collect::<Vec<_>>();
17166 assert_eq!(
17167 completion_labels,
17168 &["registered_command", "unregistered_command",],
17169 );
17170 } else {
17171 panic!("expected completion menu to be open");
17172 }
17173 });
17174
17175 editor
17176 .update_in(cx, |editor, window, cx| {
17177 editor
17178 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17179 .unwrap()
17180 })
17181 .await
17182 .unwrap();
17183 cx.run_until_parked();
17184 assert_eq!(
17185 command_calls.load(atomic::Ordering::Acquire),
17186 1,
17187 "For completion with a registered command, Zed should send a command execution request",
17188 );
17189
17190 editor.update_in(cx, |editor, window, cx| {
17191 cx.focus_self(window);
17192 editor.handle_input(".", window, cx);
17193 });
17194 cx.run_until_parked();
17195 editor.update(cx, |editor, _| {
17196 assert!(editor.context_menu_visible());
17197 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17198 {
17199 let completion_labels = menu
17200 .completions
17201 .borrow()
17202 .iter()
17203 .map(|c| c.label.text.clone())
17204 .collect::<Vec<_>>();
17205 assert_eq!(
17206 completion_labels,
17207 &["registered_command", "unregistered_command",],
17208 );
17209 } else {
17210 panic!("expected completion menu to be open");
17211 }
17212 });
17213 editor
17214 .update_in(cx, |editor, window, cx| {
17215 editor.context_menu_next(&Default::default(), window, cx);
17216 editor
17217 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17218 .unwrap()
17219 })
17220 .await
17221 .unwrap();
17222 cx.run_until_parked();
17223 assert_eq!(
17224 command_calls.load(atomic::Ordering::Acquire),
17225 1,
17226 "For completion with an unregistered command, Zed should not send a command execution request",
17227 );
17228}
17229
17230#[gpui::test]
17231async fn test_completion_reuse(cx: &mut TestAppContext) {
17232 init_test(cx, |_| {});
17233
17234 let mut cx = EditorLspTestContext::new_rust(
17235 lsp::ServerCapabilities {
17236 completion_provider: Some(lsp::CompletionOptions {
17237 trigger_characters: Some(vec![".".to_string()]),
17238 ..Default::default()
17239 }),
17240 ..Default::default()
17241 },
17242 cx,
17243 )
17244 .await;
17245
17246 let counter = Arc::new(AtomicUsize::new(0));
17247 cx.set_state("objˇ");
17248 cx.simulate_keystroke(".");
17249
17250 // Initial completion request returns complete results
17251 let is_incomplete = false;
17252 handle_completion_request(
17253 "obj.|<>",
17254 vec!["a", "ab", "abc"],
17255 is_incomplete,
17256 counter.clone(),
17257 &mut cx,
17258 )
17259 .await;
17260 cx.run_until_parked();
17261 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17262 cx.assert_editor_state("obj.ˇ");
17263 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
17264
17265 // Type "a" - filters existing completions
17266 cx.simulate_keystroke("a");
17267 cx.run_until_parked();
17268 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17269 cx.assert_editor_state("obj.aˇ");
17270 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
17271
17272 // Type "b" - filters existing completions
17273 cx.simulate_keystroke("b");
17274 cx.run_until_parked();
17275 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17276 cx.assert_editor_state("obj.abˇ");
17277 check_displayed_completions(vec!["ab", "abc"], &mut cx);
17278
17279 // Type "c" - filters existing completions
17280 cx.simulate_keystroke("c");
17281 cx.run_until_parked();
17282 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17283 cx.assert_editor_state("obj.abcˇ");
17284 check_displayed_completions(vec!["abc"], &mut cx);
17285
17286 // Backspace to delete "c" - filters existing completions
17287 cx.update_editor(|editor, window, cx| {
17288 editor.backspace(&Backspace, window, cx);
17289 });
17290 cx.run_until_parked();
17291 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17292 cx.assert_editor_state("obj.abˇ");
17293 check_displayed_completions(vec!["ab", "abc"], &mut cx);
17294
17295 // Moving cursor to the left dismisses menu.
17296 cx.update_editor(|editor, window, cx| {
17297 editor.move_left(&MoveLeft, window, cx);
17298 });
17299 cx.run_until_parked();
17300 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17301 cx.assert_editor_state("obj.aˇb");
17302 cx.update_editor(|editor, _, _| {
17303 assert_eq!(editor.context_menu_visible(), false);
17304 });
17305
17306 // Type "b" - new request
17307 cx.simulate_keystroke("b");
17308 let is_incomplete = false;
17309 handle_completion_request(
17310 "obj.<ab|>a",
17311 vec!["ab", "abc"],
17312 is_incomplete,
17313 counter.clone(),
17314 &mut cx,
17315 )
17316 .await;
17317 cx.run_until_parked();
17318 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
17319 cx.assert_editor_state("obj.abˇb");
17320 check_displayed_completions(vec!["ab", "abc"], &mut cx);
17321
17322 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
17323 cx.update_editor(|editor, window, cx| {
17324 editor.backspace(&Backspace, window, cx);
17325 });
17326 let is_incomplete = false;
17327 handle_completion_request(
17328 "obj.<a|>b",
17329 vec!["a", "ab", "abc"],
17330 is_incomplete,
17331 counter.clone(),
17332 &mut cx,
17333 )
17334 .await;
17335 cx.run_until_parked();
17336 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
17337 cx.assert_editor_state("obj.aˇb");
17338 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
17339
17340 // Backspace to delete "a" - dismisses menu.
17341 cx.update_editor(|editor, window, cx| {
17342 editor.backspace(&Backspace, window, cx);
17343 });
17344 cx.run_until_parked();
17345 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
17346 cx.assert_editor_state("obj.ˇb");
17347 cx.update_editor(|editor, _, _| {
17348 assert_eq!(editor.context_menu_visible(), false);
17349 });
17350}
17351
17352#[gpui::test]
17353async fn test_word_completion(cx: &mut TestAppContext) {
17354 let lsp_fetch_timeout_ms = 10;
17355 init_test(cx, |language_settings| {
17356 language_settings.defaults.completions = Some(CompletionSettingsContent {
17357 words_min_length: Some(0),
17358 lsp_fetch_timeout_ms: Some(10),
17359 lsp_insert_mode: Some(LspInsertMode::Insert),
17360 ..Default::default()
17361 });
17362 });
17363
17364 let mut cx = EditorLspTestContext::new_rust(
17365 lsp::ServerCapabilities {
17366 completion_provider: Some(lsp::CompletionOptions {
17367 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17368 ..lsp::CompletionOptions::default()
17369 }),
17370 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17371 ..lsp::ServerCapabilities::default()
17372 },
17373 cx,
17374 )
17375 .await;
17376
17377 let throttle_completions = Arc::new(AtomicBool::new(false));
17378
17379 let lsp_throttle_completions = throttle_completions.clone();
17380 let _completion_requests_handler =
17381 cx.lsp
17382 .server
17383 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
17384 let lsp_throttle_completions = lsp_throttle_completions.clone();
17385 let cx = cx.clone();
17386 async move {
17387 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
17388 cx.background_executor()
17389 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
17390 .await;
17391 }
17392 Ok(Some(lsp::CompletionResponse::Array(vec![
17393 lsp::CompletionItem {
17394 label: "first".into(),
17395 ..lsp::CompletionItem::default()
17396 },
17397 lsp::CompletionItem {
17398 label: "last".into(),
17399 ..lsp::CompletionItem::default()
17400 },
17401 ])))
17402 }
17403 });
17404
17405 cx.set_state(indoc! {"
17406 oneˇ
17407 two
17408 three
17409 "});
17410 cx.simulate_keystroke(".");
17411 cx.executor().run_until_parked();
17412 cx.condition(|editor, _| editor.context_menu_visible())
17413 .await;
17414 cx.update_editor(|editor, window, cx| {
17415 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17416 {
17417 assert_eq!(
17418 completion_menu_entries(menu),
17419 &["first", "last"],
17420 "When LSP server is fast to reply, no fallback word completions are used"
17421 );
17422 } else {
17423 panic!("expected completion menu to be open");
17424 }
17425 editor.cancel(&Cancel, window, cx);
17426 });
17427 cx.executor().run_until_parked();
17428 cx.condition(|editor, _| !editor.context_menu_visible())
17429 .await;
17430
17431 throttle_completions.store(true, atomic::Ordering::Release);
17432 cx.simulate_keystroke(".");
17433 cx.executor()
17434 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
17435 cx.executor().run_until_parked();
17436 cx.condition(|editor, _| editor.context_menu_visible())
17437 .await;
17438 cx.update_editor(|editor, _, _| {
17439 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17440 {
17441 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
17442 "When LSP server is slow, document words can be shown instead, if configured accordingly");
17443 } else {
17444 panic!("expected completion menu to be open");
17445 }
17446 });
17447}
17448
17449#[gpui::test]
17450async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
17451 init_test(cx, |language_settings| {
17452 language_settings.defaults.completions = Some(CompletionSettingsContent {
17453 words: Some(WordsCompletionMode::Enabled),
17454 words_min_length: Some(0),
17455 lsp_insert_mode: Some(LspInsertMode::Insert),
17456 ..Default::default()
17457 });
17458 });
17459
17460 let mut cx = EditorLspTestContext::new_rust(
17461 lsp::ServerCapabilities {
17462 completion_provider: Some(lsp::CompletionOptions {
17463 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17464 ..lsp::CompletionOptions::default()
17465 }),
17466 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17467 ..lsp::ServerCapabilities::default()
17468 },
17469 cx,
17470 )
17471 .await;
17472
17473 let _completion_requests_handler =
17474 cx.lsp
17475 .server
17476 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
17477 Ok(Some(lsp::CompletionResponse::Array(vec![
17478 lsp::CompletionItem {
17479 label: "first".into(),
17480 ..lsp::CompletionItem::default()
17481 },
17482 lsp::CompletionItem {
17483 label: "last".into(),
17484 ..lsp::CompletionItem::default()
17485 },
17486 ])))
17487 });
17488
17489 cx.set_state(indoc! {"ˇ
17490 first
17491 last
17492 second
17493 "});
17494 cx.simulate_keystroke(".");
17495 cx.executor().run_until_parked();
17496 cx.condition(|editor, _| editor.context_menu_visible())
17497 .await;
17498 cx.update_editor(|editor, _, _| {
17499 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17500 {
17501 assert_eq!(
17502 completion_menu_entries(menu),
17503 &["first", "last", "second"],
17504 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
17505 );
17506 } else {
17507 panic!("expected completion menu to be open");
17508 }
17509 });
17510}
17511
17512#[gpui::test]
17513async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
17514 init_test(cx, |language_settings| {
17515 language_settings.defaults.completions = Some(CompletionSettingsContent {
17516 words: Some(WordsCompletionMode::Disabled),
17517 words_min_length: Some(0),
17518 lsp_insert_mode: Some(LspInsertMode::Insert),
17519 ..Default::default()
17520 });
17521 });
17522
17523 let mut cx = EditorLspTestContext::new_rust(
17524 lsp::ServerCapabilities {
17525 completion_provider: Some(lsp::CompletionOptions {
17526 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17527 ..lsp::CompletionOptions::default()
17528 }),
17529 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17530 ..lsp::ServerCapabilities::default()
17531 },
17532 cx,
17533 )
17534 .await;
17535
17536 let _completion_requests_handler =
17537 cx.lsp
17538 .server
17539 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
17540 panic!("LSP completions should not be queried when dealing with word completions")
17541 });
17542
17543 cx.set_state(indoc! {"ˇ
17544 first
17545 last
17546 second
17547 "});
17548 cx.update_editor(|editor, window, cx| {
17549 editor.show_word_completions(&ShowWordCompletions, window, cx);
17550 });
17551 cx.executor().run_until_parked();
17552 cx.condition(|editor, _| editor.context_menu_visible())
17553 .await;
17554 cx.update_editor(|editor, _, _| {
17555 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17556 {
17557 assert_eq!(
17558 completion_menu_entries(menu),
17559 &["first", "last", "second"],
17560 "`ShowWordCompletions` action should show word completions"
17561 );
17562 } else {
17563 panic!("expected completion menu to be open");
17564 }
17565 });
17566
17567 cx.simulate_keystroke("l");
17568 cx.executor().run_until_parked();
17569 cx.condition(|editor, _| editor.context_menu_visible())
17570 .await;
17571 cx.update_editor(|editor, _, _| {
17572 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17573 {
17574 assert_eq!(
17575 completion_menu_entries(menu),
17576 &["last"],
17577 "After showing word completions, further editing should filter them and not query the LSP"
17578 );
17579 } else {
17580 panic!("expected completion menu to be open");
17581 }
17582 });
17583}
17584
17585#[gpui::test]
17586async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
17587 init_test(cx, |language_settings| {
17588 language_settings.defaults.completions = Some(CompletionSettingsContent {
17589 words_min_length: Some(0),
17590 lsp: Some(false),
17591 lsp_insert_mode: Some(LspInsertMode::Insert),
17592 ..Default::default()
17593 });
17594 });
17595
17596 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17597
17598 cx.set_state(indoc! {"ˇ
17599 0_usize
17600 let
17601 33
17602 4.5f32
17603 "});
17604 cx.update_editor(|editor, window, cx| {
17605 editor.show_completions(&ShowCompletions, window, cx);
17606 });
17607 cx.executor().run_until_parked();
17608 cx.condition(|editor, _| editor.context_menu_visible())
17609 .await;
17610 cx.update_editor(|editor, window, cx| {
17611 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17612 {
17613 assert_eq!(
17614 completion_menu_entries(menu),
17615 &["let"],
17616 "With no digits in the completion query, no digits should be in the word completions"
17617 );
17618 } else {
17619 panic!("expected completion menu to be open");
17620 }
17621 editor.cancel(&Cancel, window, cx);
17622 });
17623
17624 cx.set_state(indoc! {"3ˇ
17625 0_usize
17626 let
17627 3
17628 33.35f32
17629 "});
17630 cx.update_editor(|editor, window, cx| {
17631 editor.show_completions(&ShowCompletions, window, cx);
17632 });
17633 cx.executor().run_until_parked();
17634 cx.condition(|editor, _| editor.context_menu_visible())
17635 .await;
17636 cx.update_editor(|editor, _, _| {
17637 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17638 {
17639 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
17640 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
17641 } else {
17642 panic!("expected completion menu to be open");
17643 }
17644 });
17645}
17646
17647#[gpui::test]
17648async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
17649 init_test(cx, |language_settings| {
17650 language_settings.defaults.completions = Some(CompletionSettingsContent {
17651 words: Some(WordsCompletionMode::Enabled),
17652 words_min_length: Some(3),
17653 lsp_insert_mode: Some(LspInsertMode::Insert),
17654 ..Default::default()
17655 });
17656 });
17657
17658 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17659 cx.set_state(indoc! {"ˇ
17660 wow
17661 wowen
17662 wowser
17663 "});
17664 cx.simulate_keystroke("w");
17665 cx.executor().run_until_parked();
17666 cx.update_editor(|editor, _, _| {
17667 if editor.context_menu.borrow_mut().is_some() {
17668 panic!(
17669 "expected completion menu to be hidden, as words completion threshold is not met"
17670 );
17671 }
17672 });
17673
17674 cx.update_editor(|editor, window, cx| {
17675 editor.show_word_completions(&ShowWordCompletions, window, cx);
17676 });
17677 cx.executor().run_until_parked();
17678 cx.update_editor(|editor, window, cx| {
17679 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17680 {
17681 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");
17682 } else {
17683 panic!("expected completion menu to be open after the word completions are called with an action");
17684 }
17685
17686 editor.cancel(&Cancel, window, cx);
17687 });
17688 cx.update_editor(|editor, _, _| {
17689 if editor.context_menu.borrow_mut().is_some() {
17690 panic!("expected completion menu to be hidden after canceling");
17691 }
17692 });
17693
17694 cx.simulate_keystroke("o");
17695 cx.executor().run_until_parked();
17696 cx.update_editor(|editor, _, _| {
17697 if editor.context_menu.borrow_mut().is_some() {
17698 panic!(
17699 "expected completion menu to be hidden, as words completion threshold is not met still"
17700 );
17701 }
17702 });
17703
17704 cx.simulate_keystroke("w");
17705 cx.executor().run_until_parked();
17706 cx.update_editor(|editor, _, _| {
17707 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17708 {
17709 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
17710 } else {
17711 panic!("expected completion menu to be open after the word completions threshold is met");
17712 }
17713 });
17714}
17715
17716#[gpui::test]
17717async fn test_word_completions_disabled(cx: &mut TestAppContext) {
17718 init_test(cx, |language_settings| {
17719 language_settings.defaults.completions = Some(CompletionSettingsContent {
17720 words: Some(WordsCompletionMode::Enabled),
17721 words_min_length: Some(0),
17722 lsp_insert_mode: Some(LspInsertMode::Insert),
17723 ..Default::default()
17724 });
17725 });
17726
17727 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17728 cx.update_editor(|editor, _, _| {
17729 editor.disable_word_completions();
17730 });
17731 cx.set_state(indoc! {"ˇ
17732 wow
17733 wowen
17734 wowser
17735 "});
17736 cx.simulate_keystroke("w");
17737 cx.executor().run_until_parked();
17738 cx.update_editor(|editor, _, _| {
17739 if editor.context_menu.borrow_mut().is_some() {
17740 panic!(
17741 "expected completion menu to be hidden, as words completion are disabled for this editor"
17742 );
17743 }
17744 });
17745
17746 cx.update_editor(|editor, window, cx| {
17747 editor.show_word_completions(&ShowWordCompletions, window, cx);
17748 });
17749 cx.executor().run_until_parked();
17750 cx.update_editor(|editor, _, _| {
17751 if editor.context_menu.borrow_mut().is_some() {
17752 panic!(
17753 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
17754 );
17755 }
17756 });
17757}
17758
17759#[gpui::test]
17760async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
17761 init_test(cx, |language_settings| {
17762 language_settings.defaults.completions = Some(CompletionSettingsContent {
17763 words: Some(WordsCompletionMode::Disabled),
17764 words_min_length: Some(0),
17765 lsp_insert_mode: Some(LspInsertMode::Insert),
17766 ..Default::default()
17767 });
17768 });
17769
17770 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17771 cx.update_editor(|editor, _, _| {
17772 editor.set_completion_provider(None);
17773 });
17774 cx.set_state(indoc! {"ˇ
17775 wow
17776 wowen
17777 wowser
17778 "});
17779 cx.simulate_keystroke("w");
17780 cx.executor().run_until_parked();
17781 cx.update_editor(|editor, _, _| {
17782 if editor.context_menu.borrow_mut().is_some() {
17783 panic!("expected completion menu to be hidden, as disabled in settings");
17784 }
17785 });
17786}
17787
17788fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
17789 let position = || lsp::Position {
17790 line: params.text_document_position.position.line,
17791 character: params.text_document_position.position.character,
17792 };
17793 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17794 range: lsp::Range {
17795 start: position(),
17796 end: position(),
17797 },
17798 new_text: text.to_string(),
17799 }))
17800}
17801
17802#[gpui::test]
17803async fn test_multiline_completion(cx: &mut TestAppContext) {
17804 init_test(cx, |_| {});
17805
17806 let fs = FakeFs::new(cx.executor());
17807 fs.insert_tree(
17808 path!("/a"),
17809 json!({
17810 "main.ts": "a",
17811 }),
17812 )
17813 .await;
17814
17815 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17816 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17817 let typescript_language = Arc::new(Language::new(
17818 LanguageConfig {
17819 name: "TypeScript".into(),
17820 matcher: LanguageMatcher {
17821 path_suffixes: vec!["ts".to_string()],
17822 ..LanguageMatcher::default()
17823 },
17824 line_comments: vec!["// ".into()],
17825 ..LanguageConfig::default()
17826 },
17827 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17828 ));
17829 language_registry.add(typescript_language.clone());
17830 let mut fake_servers = language_registry.register_fake_lsp(
17831 "TypeScript",
17832 FakeLspAdapter {
17833 capabilities: lsp::ServerCapabilities {
17834 completion_provider: Some(lsp::CompletionOptions {
17835 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17836 ..lsp::CompletionOptions::default()
17837 }),
17838 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17839 ..lsp::ServerCapabilities::default()
17840 },
17841 // Emulate vtsls label generation
17842 label_for_completion: Some(Box::new(|item, _| {
17843 let text = if let Some(description) = item
17844 .label_details
17845 .as_ref()
17846 .and_then(|label_details| label_details.description.as_ref())
17847 {
17848 format!("{} {}", item.label, description)
17849 } else if let Some(detail) = &item.detail {
17850 format!("{} {}", item.label, detail)
17851 } else {
17852 item.label.clone()
17853 };
17854 Some(language::CodeLabel::plain(text, None))
17855 })),
17856 ..FakeLspAdapter::default()
17857 },
17858 );
17859 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
17860 let workspace = window
17861 .read_with(cx, |mw, _| mw.workspace().clone())
17862 .unwrap();
17863 let cx = &mut VisualTestContext::from_window(*window, cx);
17864 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
17865 workspace.project().update(cx, |project, cx| {
17866 project.worktrees(cx).next().unwrap().read(cx).id()
17867 })
17868 });
17869
17870 let _buffer = project
17871 .update(cx, |project, cx| {
17872 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
17873 })
17874 .await
17875 .unwrap();
17876 let editor = workspace
17877 .update_in(cx, |workspace, window, cx| {
17878 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
17879 })
17880 .await
17881 .unwrap()
17882 .downcast::<Editor>()
17883 .unwrap();
17884 let fake_server = fake_servers.next().await.unwrap();
17885 cx.run_until_parked();
17886
17887 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
17888 let multiline_label_2 = "a\nb\nc\n";
17889 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
17890 let multiline_description = "d\ne\nf\n";
17891 let multiline_detail_2 = "g\nh\ni\n";
17892
17893 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
17894 move |params, _| async move {
17895 Ok(Some(lsp::CompletionResponse::Array(vec![
17896 lsp::CompletionItem {
17897 label: multiline_label.to_string(),
17898 text_edit: gen_text_edit(¶ms, "new_text_1"),
17899 ..lsp::CompletionItem::default()
17900 },
17901 lsp::CompletionItem {
17902 label: "single line label 1".to_string(),
17903 detail: Some(multiline_detail.to_string()),
17904 text_edit: gen_text_edit(¶ms, "new_text_2"),
17905 ..lsp::CompletionItem::default()
17906 },
17907 lsp::CompletionItem {
17908 label: "single line label 2".to_string(),
17909 label_details: Some(lsp::CompletionItemLabelDetails {
17910 description: Some(multiline_description.to_string()),
17911 detail: None,
17912 }),
17913 text_edit: gen_text_edit(¶ms, "new_text_2"),
17914 ..lsp::CompletionItem::default()
17915 },
17916 lsp::CompletionItem {
17917 label: multiline_label_2.to_string(),
17918 detail: Some(multiline_detail_2.to_string()),
17919 text_edit: gen_text_edit(¶ms, "new_text_3"),
17920 ..lsp::CompletionItem::default()
17921 },
17922 lsp::CompletionItem {
17923 label: "Label with many spaces and \t but without newlines".to_string(),
17924 detail: Some(
17925 "Details with many spaces and \t but without newlines".to_string(),
17926 ),
17927 text_edit: gen_text_edit(¶ms, "new_text_4"),
17928 ..lsp::CompletionItem::default()
17929 },
17930 ])))
17931 },
17932 );
17933
17934 editor.update_in(cx, |editor, window, cx| {
17935 cx.focus_self(window);
17936 editor.move_to_end(&MoveToEnd, window, cx);
17937 editor.handle_input(".", window, cx);
17938 });
17939 cx.run_until_parked();
17940 completion_handle.next().await.unwrap();
17941
17942 editor.update(cx, |editor, _| {
17943 assert!(editor.context_menu_visible());
17944 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17945 {
17946 let completion_labels = menu
17947 .completions
17948 .borrow()
17949 .iter()
17950 .map(|c| c.label.text.clone())
17951 .collect::<Vec<_>>();
17952 assert_eq!(
17953 completion_labels,
17954 &[
17955 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
17956 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
17957 "single line label 2 d e f ",
17958 "a b c g h i ",
17959 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
17960 ],
17961 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
17962 );
17963
17964 for completion in menu
17965 .completions
17966 .borrow()
17967 .iter() {
17968 assert_eq!(
17969 completion.label.filter_range,
17970 0..completion.label.text.len(),
17971 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
17972 );
17973 }
17974 } else {
17975 panic!("expected completion menu to be open");
17976 }
17977 });
17978}
17979
17980#[gpui::test]
17981async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
17982 init_test(cx, |_| {});
17983 let mut cx = EditorLspTestContext::new_rust(
17984 lsp::ServerCapabilities {
17985 completion_provider: Some(lsp::CompletionOptions {
17986 trigger_characters: Some(vec![".".to_string()]),
17987 ..Default::default()
17988 }),
17989 ..Default::default()
17990 },
17991 cx,
17992 )
17993 .await;
17994 cx.lsp
17995 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17996 Ok(Some(lsp::CompletionResponse::Array(vec![
17997 lsp::CompletionItem {
17998 label: "first".into(),
17999 ..Default::default()
18000 },
18001 lsp::CompletionItem {
18002 label: "last".into(),
18003 ..Default::default()
18004 },
18005 ])))
18006 });
18007 cx.set_state("variableˇ");
18008 cx.simulate_keystroke(".");
18009 cx.executor().run_until_parked();
18010
18011 cx.update_editor(|editor, _, _| {
18012 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18013 {
18014 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
18015 } else {
18016 panic!("expected completion menu to be open");
18017 }
18018 });
18019
18020 cx.update_editor(|editor, window, cx| {
18021 editor.move_page_down(&MovePageDown::default(), window, cx);
18022 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18023 {
18024 assert!(
18025 menu.selected_item == 1,
18026 "expected PageDown to select the last item from the context menu"
18027 );
18028 } else {
18029 panic!("expected completion menu to stay open after PageDown");
18030 }
18031 });
18032
18033 cx.update_editor(|editor, window, cx| {
18034 editor.move_page_up(&MovePageUp::default(), window, cx);
18035 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18036 {
18037 assert!(
18038 menu.selected_item == 0,
18039 "expected PageUp to select the first item from the context menu"
18040 );
18041 } else {
18042 panic!("expected completion menu to stay open after PageUp");
18043 }
18044 });
18045}
18046
18047#[gpui::test]
18048async fn test_as_is_completions(cx: &mut TestAppContext) {
18049 init_test(cx, |_| {});
18050 let mut cx = EditorLspTestContext::new_rust(
18051 lsp::ServerCapabilities {
18052 completion_provider: Some(lsp::CompletionOptions {
18053 ..Default::default()
18054 }),
18055 ..Default::default()
18056 },
18057 cx,
18058 )
18059 .await;
18060 cx.lsp
18061 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18062 Ok(Some(lsp::CompletionResponse::Array(vec![
18063 lsp::CompletionItem {
18064 label: "unsafe".into(),
18065 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18066 range: lsp::Range {
18067 start: lsp::Position {
18068 line: 1,
18069 character: 2,
18070 },
18071 end: lsp::Position {
18072 line: 1,
18073 character: 3,
18074 },
18075 },
18076 new_text: "unsafe".to_string(),
18077 })),
18078 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
18079 ..Default::default()
18080 },
18081 ])))
18082 });
18083 cx.set_state("fn a() {}\n nˇ");
18084 cx.executor().run_until_parked();
18085 cx.update_editor(|editor, window, cx| {
18086 editor.trigger_completion_on_input("n", true, window, cx)
18087 });
18088 cx.executor().run_until_parked();
18089
18090 cx.update_editor(|editor, window, cx| {
18091 editor.confirm_completion(&Default::default(), window, cx)
18092 });
18093 cx.executor().run_until_parked();
18094 cx.assert_editor_state("fn a() {}\n unsafeˇ");
18095}
18096
18097#[gpui::test]
18098async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
18099 init_test(cx, |_| {});
18100 let language =
18101 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
18102 let mut cx = EditorLspTestContext::new(
18103 language,
18104 lsp::ServerCapabilities {
18105 completion_provider: Some(lsp::CompletionOptions {
18106 ..lsp::CompletionOptions::default()
18107 }),
18108 ..lsp::ServerCapabilities::default()
18109 },
18110 cx,
18111 )
18112 .await;
18113
18114 cx.set_state(
18115 "#ifndef BAR_H
18116#define BAR_H
18117
18118#include <stdbool.h>
18119
18120int fn_branch(bool do_branch1, bool do_branch2);
18121
18122#endif // BAR_H
18123ˇ",
18124 );
18125 cx.executor().run_until_parked();
18126 cx.update_editor(|editor, window, cx| {
18127 editor.handle_input("#", window, cx);
18128 });
18129 cx.executor().run_until_parked();
18130 cx.update_editor(|editor, window, cx| {
18131 editor.handle_input("i", window, cx);
18132 });
18133 cx.executor().run_until_parked();
18134 cx.update_editor(|editor, window, cx| {
18135 editor.handle_input("n", window, cx);
18136 });
18137 cx.executor().run_until_parked();
18138 cx.assert_editor_state(
18139 "#ifndef BAR_H
18140#define BAR_H
18141
18142#include <stdbool.h>
18143
18144int fn_branch(bool do_branch1, bool do_branch2);
18145
18146#endif // BAR_H
18147#inˇ",
18148 );
18149
18150 cx.lsp
18151 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18152 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18153 is_incomplete: false,
18154 item_defaults: None,
18155 items: vec![lsp::CompletionItem {
18156 kind: Some(lsp::CompletionItemKind::SNIPPET),
18157 label_details: Some(lsp::CompletionItemLabelDetails {
18158 detail: Some("header".to_string()),
18159 description: None,
18160 }),
18161 label: " include".to_string(),
18162 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18163 range: lsp::Range {
18164 start: lsp::Position {
18165 line: 8,
18166 character: 1,
18167 },
18168 end: lsp::Position {
18169 line: 8,
18170 character: 1,
18171 },
18172 },
18173 new_text: "include \"$0\"".to_string(),
18174 })),
18175 sort_text: Some("40b67681include".to_string()),
18176 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18177 filter_text: Some("include".to_string()),
18178 insert_text: Some("include \"$0\"".to_string()),
18179 ..lsp::CompletionItem::default()
18180 }],
18181 })))
18182 });
18183 cx.update_editor(|editor, window, cx| {
18184 editor.show_completions(&ShowCompletions, window, cx);
18185 });
18186 cx.executor().run_until_parked();
18187 cx.update_editor(|editor, window, cx| {
18188 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
18189 });
18190 cx.executor().run_until_parked();
18191 cx.assert_editor_state(
18192 "#ifndef BAR_H
18193#define BAR_H
18194
18195#include <stdbool.h>
18196
18197int fn_branch(bool do_branch1, bool do_branch2);
18198
18199#endif // BAR_H
18200#include \"ˇ\"",
18201 );
18202
18203 cx.lsp
18204 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18205 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18206 is_incomplete: true,
18207 item_defaults: None,
18208 items: vec![lsp::CompletionItem {
18209 kind: Some(lsp::CompletionItemKind::FILE),
18210 label: "AGL/".to_string(),
18211 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18212 range: lsp::Range {
18213 start: lsp::Position {
18214 line: 8,
18215 character: 10,
18216 },
18217 end: lsp::Position {
18218 line: 8,
18219 character: 11,
18220 },
18221 },
18222 new_text: "AGL/".to_string(),
18223 })),
18224 sort_text: Some("40b67681AGL/".to_string()),
18225 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18226 filter_text: Some("AGL/".to_string()),
18227 insert_text: Some("AGL/".to_string()),
18228 ..lsp::CompletionItem::default()
18229 }],
18230 })))
18231 });
18232 cx.update_editor(|editor, window, cx| {
18233 editor.show_completions(&ShowCompletions, window, cx);
18234 });
18235 cx.executor().run_until_parked();
18236 cx.update_editor(|editor, window, cx| {
18237 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
18238 });
18239 cx.executor().run_until_parked();
18240 cx.assert_editor_state(
18241 r##"#ifndef BAR_H
18242#define BAR_H
18243
18244#include <stdbool.h>
18245
18246int fn_branch(bool do_branch1, bool do_branch2);
18247
18248#endif // BAR_H
18249#include "AGL/ˇ"##,
18250 );
18251
18252 cx.update_editor(|editor, window, cx| {
18253 editor.handle_input("\"", window, cx);
18254 });
18255 cx.executor().run_until_parked();
18256 cx.assert_editor_state(
18257 r##"#ifndef BAR_H
18258#define BAR_H
18259
18260#include <stdbool.h>
18261
18262int fn_branch(bool do_branch1, bool do_branch2);
18263
18264#endif // BAR_H
18265#include "AGL/"ˇ"##,
18266 );
18267}
18268
18269#[gpui::test]
18270async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
18271 init_test(cx, |_| {});
18272
18273 let mut cx = EditorLspTestContext::new_rust(
18274 lsp::ServerCapabilities {
18275 completion_provider: Some(lsp::CompletionOptions {
18276 trigger_characters: Some(vec![".".to_string()]),
18277 resolve_provider: Some(false),
18278 ..lsp::CompletionOptions::default()
18279 }),
18280 ..lsp::ServerCapabilities::default()
18281 },
18282 cx,
18283 )
18284 .await;
18285
18286 cx.set_state("fn main() { let a = 2ˇ; }");
18287 cx.simulate_keystroke(".");
18288 let completion_item = lsp::CompletionItem {
18289 label: "Some".into(),
18290 kind: Some(lsp::CompletionItemKind::SNIPPET),
18291 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18292 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18293 kind: lsp::MarkupKind::Markdown,
18294 value: "```rust\nSome(2)\n```".to_string(),
18295 })),
18296 deprecated: Some(false),
18297 sort_text: Some("Some".to_string()),
18298 filter_text: Some("Some".to_string()),
18299 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18300 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18301 range: lsp::Range {
18302 start: lsp::Position {
18303 line: 0,
18304 character: 22,
18305 },
18306 end: lsp::Position {
18307 line: 0,
18308 character: 22,
18309 },
18310 },
18311 new_text: "Some(2)".to_string(),
18312 })),
18313 additional_text_edits: Some(vec![lsp::TextEdit {
18314 range: lsp::Range {
18315 start: lsp::Position {
18316 line: 0,
18317 character: 20,
18318 },
18319 end: lsp::Position {
18320 line: 0,
18321 character: 22,
18322 },
18323 },
18324 new_text: "".to_string(),
18325 }]),
18326 ..Default::default()
18327 };
18328
18329 let closure_completion_item = completion_item.clone();
18330 let counter = Arc::new(AtomicUsize::new(0));
18331 let counter_clone = counter.clone();
18332 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18333 let task_completion_item = closure_completion_item.clone();
18334 counter_clone.fetch_add(1, atomic::Ordering::Release);
18335 async move {
18336 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18337 is_incomplete: true,
18338 item_defaults: None,
18339 items: vec![task_completion_item],
18340 })))
18341 }
18342 });
18343
18344 cx.executor().run_until_parked();
18345 cx.condition(|editor, _| editor.context_menu_visible())
18346 .await;
18347 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
18348 assert!(request.next().await.is_some());
18349 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
18350
18351 cx.simulate_keystrokes("S o m");
18352 cx.condition(|editor, _| editor.context_menu_visible())
18353 .await;
18354 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
18355 assert!(request.next().await.is_some());
18356 assert!(request.next().await.is_some());
18357 assert!(request.next().await.is_some());
18358 request.close();
18359 assert!(request.next().await.is_none());
18360 assert_eq!(
18361 counter.load(atomic::Ordering::Acquire),
18362 4,
18363 "With the completions menu open, only one LSP request should happen per input"
18364 );
18365}
18366
18367#[gpui::test]
18368async fn test_toggle_comment(cx: &mut TestAppContext) {
18369 init_test(cx, |_| {});
18370 let mut cx = EditorTestContext::new(cx).await;
18371 let language = Arc::new(Language::new(
18372 LanguageConfig {
18373 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
18374 ..Default::default()
18375 },
18376 Some(tree_sitter_rust::LANGUAGE.into()),
18377 ));
18378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18379
18380 // If multiple selections intersect a line, the line is only toggled once.
18381 cx.set_state(indoc! {"
18382 fn a() {
18383 «//b();
18384 ˇ»// «c();
18385 //ˇ» d();
18386 }
18387 "});
18388
18389 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18390
18391 cx.assert_editor_state(indoc! {"
18392 fn a() {
18393 «b();
18394 ˇ»«c();
18395 ˇ» d();
18396 }
18397 "});
18398
18399 // The comment prefix is inserted at the same column for every line in a
18400 // selection.
18401 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18402
18403 cx.assert_editor_state(indoc! {"
18404 fn a() {
18405 // «b();
18406 ˇ»// «c();
18407 ˇ» // d();
18408 }
18409 "});
18410
18411 // If a selection ends at the beginning of a line, that line is not toggled.
18412 cx.set_selections_state(indoc! {"
18413 fn a() {
18414 // b();
18415 «// c();
18416 ˇ» // d();
18417 }
18418 "});
18419
18420 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18421
18422 cx.assert_editor_state(indoc! {"
18423 fn a() {
18424 // b();
18425 «c();
18426 ˇ» // d();
18427 }
18428 "});
18429
18430 // If a selection span a single line and is empty, the line is toggled.
18431 cx.set_state(indoc! {"
18432 fn a() {
18433 a();
18434 b();
18435 ˇ
18436 }
18437 "});
18438
18439 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18440
18441 cx.assert_editor_state(indoc! {"
18442 fn a() {
18443 a();
18444 b();
18445 //•ˇ
18446 }
18447 "});
18448
18449 // If a selection span multiple lines, empty lines are not toggled.
18450 cx.set_state(indoc! {"
18451 fn a() {
18452 «a();
18453
18454 c();ˇ»
18455 }
18456 "});
18457
18458 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18459
18460 cx.assert_editor_state(indoc! {"
18461 fn a() {
18462 // «a();
18463
18464 // c();ˇ»
18465 }
18466 "});
18467
18468 // If a selection includes multiple comment prefixes, all lines are uncommented.
18469 cx.set_state(indoc! {"
18470 fn a() {
18471 «// a();
18472 /// b();
18473 //! c();ˇ»
18474 }
18475 "});
18476
18477 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
18478
18479 cx.assert_editor_state(indoc! {"
18480 fn a() {
18481 «a();
18482 b();
18483 c();ˇ»
18484 }
18485 "});
18486}
18487
18488#[gpui::test]
18489async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
18490 init_test(cx, |_| {});
18491 let mut cx = EditorTestContext::new(cx).await;
18492 let language = Arc::new(Language::new(
18493 LanguageConfig {
18494 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
18495 ..Default::default()
18496 },
18497 Some(tree_sitter_rust::LANGUAGE.into()),
18498 ));
18499 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18500
18501 let toggle_comments = &ToggleComments {
18502 advance_downwards: false,
18503 ignore_indent: true,
18504 };
18505
18506 // If multiple selections intersect a line, the line is only toggled once.
18507 cx.set_state(indoc! {"
18508 fn a() {
18509 // «b();
18510 // c();
18511 // ˇ» d();
18512 }
18513 "});
18514
18515 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18516
18517 cx.assert_editor_state(indoc! {"
18518 fn a() {
18519 «b();
18520 c();
18521 ˇ» d();
18522 }
18523 "});
18524
18525 // The comment prefix is inserted at the beginning of each line
18526 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18527
18528 cx.assert_editor_state(indoc! {"
18529 fn a() {
18530 // «b();
18531 // c();
18532 // ˇ» d();
18533 }
18534 "});
18535
18536 // If a selection ends at the beginning of a line, that line is not toggled.
18537 cx.set_selections_state(indoc! {"
18538 fn a() {
18539 // b();
18540 // «c();
18541 ˇ»// d();
18542 }
18543 "});
18544
18545 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18546
18547 cx.assert_editor_state(indoc! {"
18548 fn a() {
18549 // b();
18550 «c();
18551 ˇ»// d();
18552 }
18553 "});
18554
18555 // If a selection span a single line and is empty, the line is toggled.
18556 cx.set_state(indoc! {"
18557 fn a() {
18558 a();
18559 b();
18560 ˇ
18561 }
18562 "});
18563
18564 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18565
18566 cx.assert_editor_state(indoc! {"
18567 fn a() {
18568 a();
18569 b();
18570 //ˇ
18571 }
18572 "});
18573
18574 // If a selection span multiple lines, empty lines are not toggled.
18575 cx.set_state(indoc! {"
18576 fn a() {
18577 «a();
18578
18579 c();ˇ»
18580 }
18581 "});
18582
18583 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18584
18585 cx.assert_editor_state(indoc! {"
18586 fn a() {
18587 // «a();
18588
18589 // c();ˇ»
18590 }
18591 "});
18592
18593 // If a selection includes multiple comment prefixes, all lines are uncommented.
18594 cx.set_state(indoc! {"
18595 fn a() {
18596 // «a();
18597 /// b();
18598 //! c();ˇ»
18599 }
18600 "});
18601
18602 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
18603
18604 cx.assert_editor_state(indoc! {"
18605 fn a() {
18606 «a();
18607 b();
18608 c();ˇ»
18609 }
18610 "});
18611}
18612
18613#[gpui::test]
18614async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
18615 init_test(cx, |_| {});
18616
18617 let language = Arc::new(Language::new(
18618 LanguageConfig {
18619 line_comments: vec!["// ".into()],
18620 ..Default::default()
18621 },
18622 Some(tree_sitter_rust::LANGUAGE.into()),
18623 ));
18624
18625 let mut cx = EditorTestContext::new(cx).await;
18626
18627 cx.language_registry().add(language.clone());
18628 cx.update_buffer(|buffer, cx| {
18629 buffer.set_language(Some(language), cx);
18630 });
18631
18632 let toggle_comments = &ToggleComments {
18633 advance_downwards: true,
18634 ignore_indent: false,
18635 };
18636
18637 // Single cursor on one line -> advance
18638 // Cursor moves horizontally 3 characters as well on non-blank line
18639 cx.set_state(indoc!(
18640 "fn a() {
18641 ˇdog();
18642 cat();
18643 }"
18644 ));
18645 cx.update_editor(|editor, window, cx| {
18646 editor.toggle_comments(toggle_comments, window, cx);
18647 });
18648 cx.assert_editor_state(indoc!(
18649 "fn a() {
18650 // dog();
18651 catˇ();
18652 }"
18653 ));
18654
18655 // Single selection on one line -> don't advance
18656 cx.set_state(indoc!(
18657 "fn a() {
18658 «dog()ˇ»;
18659 cat();
18660 }"
18661 ));
18662 cx.update_editor(|editor, window, cx| {
18663 editor.toggle_comments(toggle_comments, window, cx);
18664 });
18665 cx.assert_editor_state(indoc!(
18666 "fn a() {
18667 // «dog()ˇ»;
18668 cat();
18669 }"
18670 ));
18671
18672 // Multiple cursors on one line -> advance
18673 cx.set_state(indoc!(
18674 "fn a() {
18675 ˇdˇog();
18676 cat();
18677 }"
18678 ));
18679 cx.update_editor(|editor, window, cx| {
18680 editor.toggle_comments(toggle_comments, window, cx);
18681 });
18682 cx.assert_editor_state(indoc!(
18683 "fn a() {
18684 // dog();
18685 catˇ(ˇ);
18686 }"
18687 ));
18688
18689 // Multiple cursors on one line, with selection -> don't advance
18690 cx.set_state(indoc!(
18691 "fn a() {
18692 ˇdˇog«()ˇ»;
18693 cat();
18694 }"
18695 ));
18696 cx.update_editor(|editor, window, cx| {
18697 editor.toggle_comments(toggle_comments, window, cx);
18698 });
18699 cx.assert_editor_state(indoc!(
18700 "fn a() {
18701 // ˇdˇog«()ˇ»;
18702 cat();
18703 }"
18704 ));
18705
18706 // Single cursor on one line -> advance
18707 // Cursor moves to column 0 on blank line
18708 cx.set_state(indoc!(
18709 "fn a() {
18710 ˇdog();
18711
18712 cat();
18713 }"
18714 ));
18715 cx.update_editor(|editor, window, cx| {
18716 editor.toggle_comments(toggle_comments, window, cx);
18717 });
18718 cx.assert_editor_state(indoc!(
18719 "fn a() {
18720 // dog();
18721 ˇ
18722 cat();
18723 }"
18724 ));
18725
18726 // Single cursor on one line -> advance
18727 // Cursor starts and ends at column 0
18728 cx.set_state(indoc!(
18729 "fn a() {
18730 ˇ dog();
18731 cat();
18732 }"
18733 ));
18734 cx.update_editor(|editor, window, cx| {
18735 editor.toggle_comments(toggle_comments, window, cx);
18736 });
18737 cx.assert_editor_state(indoc!(
18738 "fn a() {
18739 // dog();
18740 ˇ cat();
18741 }"
18742 ));
18743}
18744
18745#[gpui::test]
18746async fn test_toggle_block_comment(cx: &mut TestAppContext) {
18747 init_test(cx, |_| {});
18748
18749 let mut cx = EditorTestContext::new(cx).await;
18750
18751 let html_language = Arc::new(
18752 Language::new(
18753 LanguageConfig {
18754 name: "HTML".into(),
18755 block_comment: Some(BlockCommentConfig {
18756 start: "<!-- ".into(),
18757 prefix: "".into(),
18758 end: " -->".into(),
18759 tab_size: 0,
18760 }),
18761 ..Default::default()
18762 },
18763 Some(tree_sitter_html::LANGUAGE.into()),
18764 )
18765 .with_injection_query(
18766 r#"
18767 (script_element
18768 (raw_text) @injection.content
18769 (#set! injection.language "javascript"))
18770 "#,
18771 )
18772 .unwrap(),
18773 );
18774
18775 let javascript_language = Arc::new(Language::new(
18776 LanguageConfig {
18777 name: "JavaScript".into(),
18778 line_comments: vec!["// ".into()],
18779 ..Default::default()
18780 },
18781 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18782 ));
18783
18784 cx.language_registry().add(html_language.clone());
18785 cx.language_registry().add(javascript_language);
18786 cx.update_buffer(|buffer, cx| {
18787 buffer.set_language(Some(html_language), cx);
18788 });
18789
18790 // Toggle comments for empty selections
18791 cx.set_state(
18792 &r#"
18793 <p>A</p>ˇ
18794 <p>B</p>ˇ
18795 <p>C</p>ˇ
18796 "#
18797 .unindent(),
18798 );
18799 cx.update_editor(|editor, window, cx| {
18800 editor.toggle_comments(&ToggleComments::default(), window, cx)
18801 });
18802 cx.assert_editor_state(
18803 &r#"
18804 <!-- <p>A</p>ˇ -->
18805 <!-- <p>B</p>ˇ -->
18806 <!-- <p>C</p>ˇ -->
18807 "#
18808 .unindent(),
18809 );
18810 cx.update_editor(|editor, window, cx| {
18811 editor.toggle_comments(&ToggleComments::default(), window, cx)
18812 });
18813 cx.assert_editor_state(
18814 &r#"
18815 <p>A</p>ˇ
18816 <p>B</p>ˇ
18817 <p>C</p>ˇ
18818 "#
18819 .unindent(),
18820 );
18821
18822 // Toggle comments for mixture of empty and non-empty selections, where
18823 // multiple selections occupy a given line.
18824 cx.set_state(
18825 &r#"
18826 <p>A«</p>
18827 <p>ˇ»B</p>ˇ
18828 <p>C«</p>
18829 <p>ˇ»D</p>ˇ
18830 "#
18831 .unindent(),
18832 );
18833
18834 cx.update_editor(|editor, window, cx| {
18835 editor.toggle_comments(&ToggleComments::default(), window, cx)
18836 });
18837 cx.assert_editor_state(
18838 &r#"
18839 <!-- <p>A«</p>
18840 <p>ˇ»B</p>ˇ -->
18841 <!-- <p>C«</p>
18842 <p>ˇ»D</p>ˇ -->
18843 "#
18844 .unindent(),
18845 );
18846 cx.update_editor(|editor, window, cx| {
18847 editor.toggle_comments(&ToggleComments::default(), window, cx)
18848 });
18849 cx.assert_editor_state(
18850 &r#"
18851 <p>A«</p>
18852 <p>ˇ»B</p>ˇ
18853 <p>C«</p>
18854 <p>ˇ»D</p>ˇ
18855 "#
18856 .unindent(),
18857 );
18858
18859 // Toggle comments when different languages are active for different
18860 // selections.
18861 cx.set_state(
18862 &r#"
18863 ˇ<script>
18864 ˇvar x = new Y();
18865 ˇ</script>
18866 "#
18867 .unindent(),
18868 );
18869 cx.executor().run_until_parked();
18870 cx.update_editor(|editor, window, cx| {
18871 editor.toggle_comments(&ToggleComments::default(), window, cx)
18872 });
18873 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
18874 // Uncommenting and commenting from this position brings in even more wrong artifacts.
18875 cx.assert_editor_state(
18876 &r#"
18877 <!-- ˇ<script> -->
18878 // ˇvar x = new Y();
18879 <!-- ˇ</script> -->
18880 "#
18881 .unindent(),
18882 );
18883}
18884
18885#[gpui::test]
18886fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
18887 init_test(cx, |_| {});
18888
18889 let buffer = cx.new(|cx| Buffer::local(sample_text(6, 4, 'a'), cx));
18890 let multibuffer = cx.new(|cx| {
18891 let mut multibuffer = MultiBuffer::new(ReadWrite);
18892 multibuffer.set_excerpts_for_path(
18893 PathKey::sorted(0),
18894 buffer.clone(),
18895 [
18896 Point::new(0, 0)..Point::new(0, 4),
18897 Point::new(5, 0)..Point::new(5, 4),
18898 ],
18899 0,
18900 cx,
18901 );
18902 assert_eq!(multibuffer.read(cx).text(), "aaaa\nffff");
18903 multibuffer
18904 });
18905
18906 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
18907 editor.update_in(cx, |editor, window, cx| {
18908 assert_eq!(editor.text(cx), "aaaa\nffff");
18909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18910 s.select_ranges([
18911 Point::new(0, 0)..Point::new(0, 0),
18912 Point::new(1, 0)..Point::new(1, 0),
18913 ])
18914 });
18915
18916 editor.handle_input("X", window, cx);
18917 assert_eq!(editor.text(cx), "Xaaaa\nXffff");
18918 assert_eq!(
18919 editor.selections.ranges(&editor.display_snapshot(cx)),
18920 [
18921 Point::new(0, 1)..Point::new(0, 1),
18922 Point::new(1, 1)..Point::new(1, 1),
18923 ]
18924 );
18925
18926 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
18927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18928 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
18929 });
18930 editor.backspace(&Default::default(), window, cx);
18931 assert_eq!(editor.text(cx), "Xa\nfff");
18932 assert_eq!(
18933 editor.selections.ranges(&editor.display_snapshot(cx)),
18934 [Point::new(1, 0)..Point::new(1, 0)]
18935 );
18936
18937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18938 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
18939 });
18940 editor.backspace(&Default::default(), window, cx);
18941 assert_eq!(editor.text(cx), "X\nff");
18942 assert_eq!(
18943 editor.selections.ranges(&editor.display_snapshot(cx)),
18944 [Point::new(0, 1)..Point::new(0, 1)]
18945 );
18946 });
18947}
18948
18949#[gpui::test]
18950async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
18951 init_test(cx, |_| {});
18952
18953 let language = Arc::new(
18954 Language::new(
18955 LanguageConfig {
18956 brackets: BracketPairConfig {
18957 pairs: vec![
18958 BracketPair {
18959 start: "{".to_string(),
18960 end: "}".to_string(),
18961 close: true,
18962 surround: true,
18963 newline: true,
18964 },
18965 BracketPair {
18966 start: "/* ".to_string(),
18967 end: " */".to_string(),
18968 close: true,
18969 surround: true,
18970 newline: true,
18971 },
18972 ],
18973 ..Default::default()
18974 },
18975 ..Default::default()
18976 },
18977 Some(tree_sitter_rust::LANGUAGE.into()),
18978 )
18979 .with_indents_query("")
18980 .unwrap(),
18981 );
18982
18983 let text = concat!(
18984 "{ }\n", //
18985 " x\n", //
18986 " /* */\n", //
18987 "x\n", //
18988 "{{} }\n", //
18989 );
18990
18991 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18992 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18993 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18994 editor
18995 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
18996 .await;
18997
18998 editor.update_in(cx, |editor, window, cx| {
18999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19000 s.select_display_ranges([
19001 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
19002 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
19003 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
19004 ])
19005 });
19006 editor.newline(&Newline, window, cx);
19007
19008 assert_eq!(
19009 editor.buffer().read(cx).read(cx).text(),
19010 concat!(
19011 "{ \n", // Suppress rustfmt
19012 "\n", //
19013 "}\n", //
19014 " x\n", //
19015 " /* \n", //
19016 " \n", //
19017 " */\n", //
19018 "x\n", //
19019 "{{} \n", //
19020 "}\n", //
19021 )
19022 );
19023 });
19024}
19025
19026#[gpui::test]
19027fn test_highlighted_ranges(cx: &mut TestAppContext) {
19028 init_test(cx, |_| {});
19029
19030 let editor = cx.add_window(|window, cx| {
19031 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
19032 build_editor(buffer, window, cx)
19033 });
19034
19035 _ = editor.update(cx, |editor, window, cx| {
19036 let buffer = editor.buffer.read(cx).snapshot(cx);
19037
19038 let anchor_range =
19039 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
19040
19041 editor.highlight_background(
19042 HighlightKey::ColorizeBracket(0),
19043 &[
19044 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
19045 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
19046 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
19047 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
19048 ],
19049 |_, _| Hsla::red(),
19050 cx,
19051 );
19052 editor.highlight_background(
19053 HighlightKey::ColorizeBracket(1),
19054 &[
19055 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
19056 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
19057 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
19058 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
19059 ],
19060 |_, _| Hsla::green(),
19061 cx,
19062 );
19063
19064 let snapshot = editor.snapshot(window, cx);
19065 let highlighted_ranges = editor.sorted_background_highlights_in_range(
19066 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
19067 &snapshot,
19068 cx.theme(),
19069 );
19070 assert_eq!(
19071 highlighted_ranges,
19072 &[
19073 (
19074 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
19075 Hsla::green(),
19076 ),
19077 (
19078 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
19079 Hsla::red(),
19080 ),
19081 (
19082 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
19083 Hsla::green(),
19084 ),
19085 (
19086 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
19087 Hsla::red(),
19088 ),
19089 ]
19090 );
19091 assert_eq!(
19092 editor.sorted_background_highlights_in_range(
19093 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
19094 &snapshot,
19095 cx.theme(),
19096 ),
19097 &[(
19098 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
19099 Hsla::red(),
19100 )]
19101 );
19102 });
19103}
19104
19105#[gpui::test]
19106async fn test_copy_highlight_json(cx: &mut TestAppContext) {
19107 init_test(cx, |_| {});
19108
19109 let mut cx = EditorTestContext::new(cx).await;
19110 cx.set_state(indoc! {"
19111 fn main() {
19112 let x = 1;ˇ
19113 }
19114 "});
19115 setup_rust_syntax_highlighting(&mut cx);
19116
19117 cx.update_editor(|editor, window, cx| {
19118 editor.copy_highlight_json(&CopyHighlightJson, window, cx);
19119 });
19120
19121 let clipboard_json: serde_json::Value =
19122 serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap();
19123 assert_eq!(
19124 clipboard_json,
19125 json!([
19126 [
19127 {"text": "fn", "highlight": "keyword"},
19128 {"text": " ", "highlight": null},
19129 {"text": "main", "highlight": "function"},
19130 {"text": "()", "highlight": "punctuation.bracket"},
19131 {"text": " ", "highlight": null},
19132 {"text": "{", "highlight": "punctuation.bracket"},
19133 ],
19134 [
19135 {"text": " ", "highlight": null},
19136 {"text": "let", "highlight": "keyword"},
19137 {"text": " ", "highlight": null},
19138 {"text": "x", "highlight": "variable"},
19139 {"text": " ", "highlight": null},
19140 {"text": "=", "highlight": "operator"},
19141 {"text": " ", "highlight": null},
19142 {"text": "1", "highlight": "number"},
19143 {"text": ";", "highlight": "punctuation.delimiter"},
19144 ],
19145 [
19146 {"text": "}", "highlight": "punctuation.bracket"},
19147 ],
19148 ])
19149 );
19150}
19151
19152#[gpui::test]
19153async fn test_copy_highlight_json_selected_range(cx: &mut TestAppContext) {
19154 init_test(cx, |_| {});
19155
19156 let mut cx = EditorTestContext::new(cx).await;
19157 cx.set_state(indoc! {"
19158 fn main() {
19159 «let x = 1;
19160 let yˇ» = 2;
19161 }
19162 "});
19163 setup_rust_syntax_highlighting(&mut cx);
19164
19165 cx.update_editor(|editor, window, cx| {
19166 editor.copy_highlight_json(&CopyHighlightJson, window, cx);
19167 });
19168
19169 let clipboard_json: serde_json::Value =
19170 serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap();
19171 assert_eq!(
19172 clipboard_json,
19173 json!([
19174 [
19175 {"text": "let", "highlight": "keyword"},
19176 {"text": " ", "highlight": null},
19177 {"text": "x", "highlight": "variable"},
19178 {"text": " ", "highlight": null},
19179 {"text": "=", "highlight": "operator"},
19180 {"text": " ", "highlight": null},
19181 {"text": "1", "highlight": "number"},
19182 {"text": ";", "highlight": "punctuation.delimiter"},
19183 ],
19184 [
19185 {"text": " ", "highlight": null},
19186 {"text": "let", "highlight": "keyword"},
19187 {"text": " ", "highlight": null},
19188 {"text": "y", "highlight": "variable"},
19189 ],
19190 ])
19191 );
19192}
19193
19194#[gpui::test]
19195async fn test_copy_highlight_json_selected_line_range(cx: &mut TestAppContext) {
19196 init_test(cx, |_| {});
19197
19198 let mut cx = EditorTestContext::new(cx).await;
19199
19200 cx.set_state(indoc! {"
19201 fn main() {
19202 «let x = 1;
19203 let yˇ» = 2;
19204 }
19205 "});
19206 setup_rust_syntax_highlighting(&mut cx);
19207
19208 cx.update_editor(|editor, window, cx| {
19209 editor.selections.set_line_mode(true);
19210 editor.copy_highlight_json(&CopyHighlightJson, window, cx);
19211 });
19212
19213 let clipboard_json: serde_json::Value =
19214 serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap();
19215 assert_eq!(
19216 clipboard_json,
19217 json!([
19218 [
19219 {"text": " ", "highlight": null},
19220 {"text": "let", "highlight": "keyword"},
19221 {"text": " ", "highlight": null},
19222 {"text": "x", "highlight": "variable"},
19223 {"text": " ", "highlight": null},
19224 {"text": "=", "highlight": "operator"},
19225 {"text": " ", "highlight": null},
19226 {"text": "1", "highlight": "number"},
19227 {"text": ";", "highlight": "punctuation.delimiter"},
19228 ],
19229 [
19230 {"text": " ", "highlight": null},
19231 {"text": "let", "highlight": "keyword"},
19232 {"text": " ", "highlight": null},
19233 {"text": "y", "highlight": "variable"},
19234 {"text": " ", "highlight": null},
19235 {"text": "=", "highlight": "operator"},
19236 {"text": " ", "highlight": null},
19237 {"text": "2", "highlight": "number"},
19238 {"text": ";", "highlight": "punctuation.delimiter"},
19239 ],
19240 ])
19241 );
19242}
19243
19244#[gpui::test]
19245async fn test_copy_highlight_json_single_line(cx: &mut TestAppContext) {
19246 init_test(cx, |_| {});
19247
19248 let mut cx = EditorTestContext::new(cx).await;
19249
19250 cx.set_state(indoc! {"
19251 fn main() {
19252 let ˇx = 1;
19253 let y = 2;
19254 }
19255 "});
19256 setup_rust_syntax_highlighting(&mut cx);
19257
19258 cx.update_editor(|editor, window, cx| {
19259 editor.selections.set_line_mode(true);
19260 editor.copy_highlight_json(&CopyHighlightJson, window, cx);
19261 });
19262
19263 let clipboard_json: serde_json::Value =
19264 serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap();
19265 assert_eq!(
19266 clipboard_json,
19267 json!([
19268 [
19269 {"text": " ", "highlight": null},
19270 {"text": "let", "highlight": "keyword"},
19271 {"text": " ", "highlight": null},
19272 {"text": "x", "highlight": "variable"},
19273 {"text": " ", "highlight": null},
19274 {"text": "=", "highlight": "operator"},
19275 {"text": " ", "highlight": null},
19276 {"text": "1", "highlight": "number"},
19277 {"text": ";", "highlight": "punctuation.delimiter"},
19278 ]
19279 ])
19280 );
19281}
19282
19283fn setup_rust_syntax_highlighting(cx: &mut EditorTestContext) {
19284 let syntax = SyntaxTheme::new_test(vec![
19285 ("keyword", Hsla::red()),
19286 ("function", Hsla::blue()),
19287 ("variable", Hsla::green()),
19288 ("number", Hsla::default()),
19289 ("operator", Hsla::default()),
19290 ("punctuation.bracket", Hsla::default()),
19291 ("punctuation.delimiter", Hsla::default()),
19292 ]);
19293
19294 let language = rust_lang();
19295 language.set_theme(&syntax);
19296
19297 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19298 cx.executor().run_until_parked();
19299 cx.update_editor(|editor, window, cx| {
19300 editor.set_style(
19301 EditorStyle {
19302 syntax: Arc::new(syntax),
19303 ..Default::default()
19304 },
19305 window,
19306 cx,
19307 );
19308 });
19309}
19310
19311#[gpui::test]
19312async fn test_following(cx: &mut TestAppContext) {
19313 init_test(cx, |_| {});
19314
19315 let fs = FakeFs::new(cx.executor());
19316 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
19317
19318 let buffer = project.update(cx, |project, cx| {
19319 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
19320 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
19321 });
19322 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
19323 let follower = cx.update(|cx| {
19324 cx.open_window(
19325 WindowOptions {
19326 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
19327 gpui::Point::new(px(0.), px(0.)),
19328 gpui::Point::new(px(10.), px(80.)),
19329 ))),
19330 ..Default::default()
19331 },
19332 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
19333 )
19334 .unwrap()
19335 });
19336
19337 let is_still_following = Rc::new(RefCell::new(true));
19338 let follower_edit_event_count = Rc::new(RefCell::new(0));
19339 let pending_update = Rc::new(RefCell::new(None));
19340 let leader_entity = leader.root(cx).unwrap();
19341 let follower_entity = follower.root(cx).unwrap();
19342 _ = follower.update(cx, {
19343 let update = pending_update.clone();
19344 let is_still_following = is_still_following.clone();
19345 let follower_edit_event_count = follower_edit_event_count.clone();
19346 |_, window, cx| {
19347 cx.subscribe_in(
19348 &leader_entity,
19349 window,
19350 move |_, leader, event, window, cx| {
19351 leader.update(cx, |leader, cx| {
19352 leader.add_event_to_update_proto(
19353 event,
19354 &mut update.borrow_mut(),
19355 window,
19356 cx,
19357 );
19358 });
19359 },
19360 )
19361 .detach();
19362
19363 cx.subscribe_in(
19364 &follower_entity,
19365 window,
19366 move |_, _, event: &EditorEvent, _window, _cx| {
19367 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
19368 *is_still_following.borrow_mut() = false;
19369 }
19370
19371 if let EditorEvent::BufferEdited = event {
19372 *follower_edit_event_count.borrow_mut() += 1;
19373 }
19374 },
19375 )
19376 .detach();
19377 }
19378 });
19379
19380 // Update the selections only
19381 _ = leader.update(cx, |leader, window, cx| {
19382 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19383 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
19384 });
19385 });
19386 follower
19387 .update(cx, |follower, window, cx| {
19388 follower.apply_update_proto(
19389 &project,
19390 pending_update.borrow_mut().take().unwrap(),
19391 window,
19392 cx,
19393 )
19394 })
19395 .unwrap()
19396 .await
19397 .unwrap();
19398 _ = follower.update(cx, |follower, _, cx| {
19399 assert_eq!(
19400 follower.selections.ranges(&follower.display_snapshot(cx)),
19401 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
19402 );
19403 });
19404 assert!(*is_still_following.borrow());
19405 assert_eq!(*follower_edit_event_count.borrow(), 0);
19406
19407 // Update the scroll position only
19408 _ = leader.update(cx, |leader, window, cx| {
19409 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
19410 });
19411 follower
19412 .update(cx, |follower, window, cx| {
19413 follower.apply_update_proto(
19414 &project,
19415 pending_update.borrow_mut().take().unwrap(),
19416 window,
19417 cx,
19418 )
19419 })
19420 .unwrap()
19421 .await
19422 .unwrap();
19423 assert_eq!(
19424 follower
19425 .update(cx, |follower, _, cx| follower.scroll_position(cx))
19426 .unwrap(),
19427 gpui::Point::new(1.5, 3.5)
19428 );
19429 assert!(*is_still_following.borrow());
19430 assert_eq!(*follower_edit_event_count.borrow(), 0);
19431
19432 // Update the selections and scroll position. The follower's scroll position is updated
19433 // via autoscroll, not via the leader's exact scroll position.
19434 _ = leader.update(cx, |leader, window, cx| {
19435 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19436 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
19437 });
19438 leader.request_autoscroll(Autoscroll::newest(), cx);
19439 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
19440 });
19441 follower
19442 .update(cx, |follower, window, cx| {
19443 follower.apply_update_proto(
19444 &project,
19445 pending_update.borrow_mut().take().unwrap(),
19446 window,
19447 cx,
19448 )
19449 })
19450 .unwrap()
19451 .await
19452 .unwrap();
19453 _ = follower.update(cx, |follower, _, cx| {
19454 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
19455 assert_eq!(
19456 follower.selections.ranges(&follower.display_snapshot(cx)),
19457 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
19458 );
19459 });
19460 assert!(*is_still_following.borrow());
19461
19462 // Creating a pending selection that precedes another selection
19463 _ = leader.update(cx, |leader, window, cx| {
19464 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19465 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
19466 });
19467 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
19468 });
19469 follower
19470 .update(cx, |follower, window, cx| {
19471 follower.apply_update_proto(
19472 &project,
19473 pending_update.borrow_mut().take().unwrap(),
19474 window,
19475 cx,
19476 )
19477 })
19478 .unwrap()
19479 .await
19480 .unwrap();
19481 _ = follower.update(cx, |follower, _, cx| {
19482 assert_eq!(
19483 follower.selections.ranges(&follower.display_snapshot(cx)),
19484 vec![
19485 MultiBufferOffset(0)..MultiBufferOffset(0),
19486 MultiBufferOffset(1)..MultiBufferOffset(1)
19487 ]
19488 );
19489 });
19490 assert!(*is_still_following.borrow());
19491
19492 // Extend the pending selection so that it surrounds another selection
19493 _ = leader.update(cx, |leader, window, cx| {
19494 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
19495 });
19496 follower
19497 .update(cx, |follower, window, cx| {
19498 follower.apply_update_proto(
19499 &project,
19500 pending_update.borrow_mut().take().unwrap(),
19501 window,
19502 cx,
19503 )
19504 })
19505 .unwrap()
19506 .await
19507 .unwrap();
19508 _ = follower.update(cx, |follower, _, cx| {
19509 assert_eq!(
19510 follower.selections.ranges(&follower.display_snapshot(cx)),
19511 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
19512 );
19513 });
19514
19515 // Scrolling locally breaks the follow
19516 _ = follower.update(cx, |follower, window, cx| {
19517 let top_anchor = follower
19518 .buffer()
19519 .read(cx)
19520 .read(cx)
19521 .anchor_after(MultiBufferOffset(0));
19522 follower.set_scroll_anchor(
19523 ScrollAnchor {
19524 anchor: top_anchor,
19525 offset: gpui::Point::new(0.0, 0.5),
19526 },
19527 window,
19528 cx,
19529 );
19530 });
19531 assert!(!(*is_still_following.borrow()));
19532}
19533
19534#[gpui::test]
19535async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
19536 init_test(cx, |_| {});
19537
19538 let fs = FakeFs::new(cx.executor());
19539 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
19540 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19541 let workspace = window
19542 .read_with(cx, |mw, _| mw.workspace().clone())
19543 .unwrap();
19544 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
19545
19546 let cx = &mut VisualTestContext::from_window(*window, cx);
19547
19548 let leader = pane.update_in(cx, |_, window, cx| {
19549 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
19550 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
19551 });
19552
19553 // Start following the editor when it has no excerpts.
19554 let mut state_message =
19555 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
19556 let workspace_entity = workspace.clone();
19557 let follower_1 = cx
19558 .update_window(*window, |_, window, cx| {
19559 Editor::from_state_proto(
19560 workspace_entity,
19561 ViewId {
19562 creator: CollaboratorId::PeerId(PeerId::default()),
19563 id: 0,
19564 },
19565 &mut state_message,
19566 window,
19567 cx,
19568 )
19569 })
19570 .unwrap()
19571 .unwrap()
19572 .await
19573 .unwrap();
19574
19575 let update_message = Rc::new(RefCell::new(None));
19576 follower_1.update_in(cx, {
19577 let update = update_message.clone();
19578 |_, window, cx| {
19579 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
19580 leader.update(cx, |leader, cx| {
19581 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
19582 });
19583 })
19584 .detach();
19585 }
19586 });
19587
19588 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
19589 (
19590 project.create_local_buffer("abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyza\nbcd\nefg\nhij\nklm\nnop\nqrs\ntuv\nwxy\nzab\ncde\nfgh\n", None, false, cx),
19591 project.create_local_buffer("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\n", None, false, cx),
19592 )
19593 });
19594
19595 // Insert some excerpts.
19596 leader.update(cx, |leader, cx| {
19597 leader.buffer.update(cx, |multibuffer, cx| {
19598 multibuffer.set_excerpts_for_path(
19599 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
19600 buffer_1.clone(),
19601 vec![
19602 Point::row_range(0..3),
19603 Point::row_range(1..6),
19604 Point::row_range(12..15),
19605 ],
19606 0,
19607 cx,
19608 );
19609 multibuffer.set_excerpts_for_path(
19610 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
19611 buffer_2.clone(),
19612 vec![Point::row_range(0..6), Point::row_range(8..12)],
19613 0,
19614 cx,
19615 );
19616 });
19617 });
19618
19619 // Apply the update of adding the excerpts.
19620 follower_1
19621 .update_in(cx, |follower, window, cx| {
19622 follower.apply_update_proto(
19623 &project,
19624 update_message.borrow().clone().unwrap(),
19625 window,
19626 cx,
19627 )
19628 })
19629 .await
19630 .unwrap();
19631 assert_eq!(
19632 follower_1.update(cx, |editor, cx| editor.text(cx)),
19633 leader.update(cx, |editor, cx| editor.text(cx))
19634 );
19635 update_message.borrow_mut().take();
19636
19637 // Start following separately after it already has excerpts.
19638 let mut state_message =
19639 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
19640 let workspace_entity = workspace.clone();
19641 let follower_2 = cx
19642 .update_window(*window, |_, window, cx| {
19643 Editor::from_state_proto(
19644 workspace_entity,
19645 ViewId {
19646 creator: CollaboratorId::PeerId(PeerId::default()),
19647 id: 0,
19648 },
19649 &mut state_message,
19650 window,
19651 cx,
19652 )
19653 })
19654 .unwrap()
19655 .unwrap()
19656 .await
19657 .unwrap();
19658 assert_eq!(
19659 follower_2.update(cx, |editor, cx| editor.text(cx)),
19660 leader.update(cx, |editor, cx| editor.text(cx))
19661 );
19662
19663 // Remove some excerpts.
19664 leader.update(cx, |leader, cx| {
19665 leader.buffer.update(cx, |multibuffer, cx| {
19666 multibuffer.remove_excerpts(
19667 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
19668 cx,
19669 );
19670 });
19671 });
19672
19673 // Apply the update of removing the excerpts.
19674 follower_1
19675 .update_in(cx, |follower, window, cx| {
19676 follower.apply_update_proto(
19677 &project,
19678 update_message.borrow().clone().unwrap(),
19679 window,
19680 cx,
19681 )
19682 })
19683 .await
19684 .unwrap();
19685 follower_2
19686 .update_in(cx, |follower, window, cx| {
19687 follower.apply_update_proto(
19688 &project,
19689 update_message.borrow().clone().unwrap(),
19690 window,
19691 cx,
19692 )
19693 })
19694 .await
19695 .unwrap();
19696 update_message.borrow_mut().take();
19697 assert_eq!(
19698 follower_1.update(cx, |editor, cx| editor.text(cx)),
19699 leader.update(cx, |editor, cx| editor.text(cx))
19700 );
19701}
19702
19703#[gpui::test]
19704async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19705 init_test(cx, |_| {});
19706
19707 let mut cx = EditorTestContext::new(cx).await;
19708 let lsp_store =
19709 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
19710
19711 cx.set_state(indoc! {"
19712 ˇfn func(abc def: i32) -> u32 {
19713 }
19714 "});
19715
19716 cx.update(|_, cx| {
19717 lsp_store.update(cx, |lsp_store, cx| {
19718 lsp_store
19719 .update_diagnostics(
19720 LanguageServerId(0),
19721 lsp::PublishDiagnosticsParams {
19722 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
19723 version: None,
19724 diagnostics: vec![
19725 lsp::Diagnostic {
19726 range: lsp::Range::new(
19727 lsp::Position::new(0, 11),
19728 lsp::Position::new(0, 12),
19729 ),
19730 severity: Some(lsp::DiagnosticSeverity::ERROR),
19731 ..Default::default()
19732 },
19733 lsp::Diagnostic {
19734 range: lsp::Range::new(
19735 lsp::Position::new(0, 12),
19736 lsp::Position::new(0, 15),
19737 ),
19738 severity: Some(lsp::DiagnosticSeverity::ERROR),
19739 ..Default::default()
19740 },
19741 lsp::Diagnostic {
19742 range: lsp::Range::new(
19743 lsp::Position::new(0, 25),
19744 lsp::Position::new(0, 28),
19745 ),
19746 severity: Some(lsp::DiagnosticSeverity::ERROR),
19747 ..Default::default()
19748 },
19749 ],
19750 },
19751 None,
19752 DiagnosticSourceKind::Pushed,
19753 &[],
19754 cx,
19755 )
19756 .unwrap()
19757 });
19758 });
19759
19760 executor.run_until_parked();
19761
19762 cx.update_editor(|editor, window, cx| {
19763 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19764 });
19765
19766 cx.assert_editor_state(indoc! {"
19767 fn func(abc def: i32) -> ˇu32 {
19768 }
19769 "});
19770
19771 cx.update_editor(|editor, window, cx| {
19772 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19773 });
19774
19775 cx.assert_editor_state(indoc! {"
19776 fn func(abc ˇdef: i32) -> u32 {
19777 }
19778 "});
19779
19780 cx.update_editor(|editor, window, cx| {
19781 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19782 });
19783
19784 cx.assert_editor_state(indoc! {"
19785 fn func(abcˇ def: i32) -> u32 {
19786 }
19787 "});
19788
19789 cx.update_editor(|editor, window, cx| {
19790 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19791 });
19792
19793 cx.assert_editor_state(indoc! {"
19794 fn func(abc def: i32) -> ˇu32 {
19795 }
19796 "});
19797}
19798
19799#[gpui::test]
19800async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19801 init_test(cx, |_| {});
19802
19803 let mut cx = EditorTestContext::new(cx).await;
19804
19805 let diff_base = r#"
19806 use some::mod;
19807
19808 const A: u32 = 42;
19809
19810 fn main() {
19811 println!("hello");
19812
19813 println!("world");
19814 }
19815 "#
19816 .unindent();
19817
19818 // Edits are modified, removed, modified, added
19819 cx.set_state(
19820 &r#"
19821 use some::modified;
19822
19823 ˇ
19824 fn main() {
19825 println!("hello there");
19826
19827 println!("around the");
19828 println!("world");
19829 }
19830 "#
19831 .unindent(),
19832 );
19833
19834 cx.set_head_text(&diff_base);
19835 executor.run_until_parked();
19836
19837 cx.update_editor(|editor, window, cx| {
19838 //Wrap around the bottom of the buffer
19839 for _ in 0..3 {
19840 editor.go_to_next_hunk(&GoToHunk, window, cx);
19841 }
19842 });
19843
19844 cx.assert_editor_state(
19845 &r#"
19846 ˇuse some::modified;
19847
19848
19849 fn main() {
19850 println!("hello there");
19851
19852 println!("around the");
19853 println!("world");
19854 }
19855 "#
19856 .unindent(),
19857 );
19858
19859 cx.update_editor(|editor, window, cx| {
19860 //Wrap around the top of the buffer
19861 for _ in 0..2 {
19862 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19863 }
19864 });
19865
19866 cx.assert_editor_state(
19867 &r#"
19868 use some::modified;
19869
19870
19871 fn main() {
19872 ˇ println!("hello there");
19873
19874 println!("around the");
19875 println!("world");
19876 }
19877 "#
19878 .unindent(),
19879 );
19880
19881 cx.update_editor(|editor, window, cx| {
19882 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19883 });
19884
19885 cx.assert_editor_state(
19886 &r#"
19887 use some::modified;
19888
19889 ˇ
19890 fn main() {
19891 println!("hello there");
19892
19893 println!("around the");
19894 println!("world");
19895 }
19896 "#
19897 .unindent(),
19898 );
19899
19900 cx.update_editor(|editor, window, cx| {
19901 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19902 });
19903
19904 cx.assert_editor_state(
19905 &r#"
19906 ˇuse some::modified;
19907
19908
19909 fn main() {
19910 println!("hello there");
19911
19912 println!("around the");
19913 println!("world");
19914 }
19915 "#
19916 .unindent(),
19917 );
19918
19919 cx.update_editor(|editor, window, cx| {
19920 for _ in 0..2 {
19921 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19922 }
19923 });
19924
19925 cx.assert_editor_state(
19926 &r#"
19927 use some::modified;
19928
19929
19930 fn main() {
19931 ˇ println!("hello there");
19932
19933 println!("around the");
19934 println!("world");
19935 }
19936 "#
19937 .unindent(),
19938 );
19939
19940 cx.update_editor(|editor, window, cx| {
19941 editor.fold(&Fold, window, cx);
19942 });
19943
19944 cx.update_editor(|editor, window, cx| {
19945 editor.go_to_next_hunk(&GoToHunk, window, cx);
19946 });
19947
19948 cx.assert_editor_state(
19949 &r#"
19950 ˇuse some::modified;
19951
19952
19953 fn main() {
19954 println!("hello there");
19955
19956 println!("around the");
19957 println!("world");
19958 }
19959 "#
19960 .unindent(),
19961 );
19962}
19963
19964#[test]
19965fn test_split_words() {
19966 fn split(text: &str) -> Vec<&str> {
19967 split_words(text).collect()
19968 }
19969
19970 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
19971 assert_eq!(split("hello_world"), &["hello_", "world"]);
19972 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
19973 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
19974 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
19975 assert_eq!(split("helloworld"), &["helloworld"]);
19976
19977 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
19978}
19979
19980#[test]
19981fn test_split_words_for_snippet_prefix() {
19982 fn split(text: &str) -> Vec<&str> {
19983 snippet_candidate_suffixes(text, &|c| c.is_alphanumeric() || c == '_').collect()
19984 }
19985
19986 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
19987 assert_eq!(split("hello_world"), &["hello_world"]);
19988 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
19989 assert_eq!(split("Hello_World"), &["Hello_World"]);
19990 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
19991 assert_eq!(split("helloworld"), &["helloworld"]);
19992 assert_eq!(
19993 split("this@is!@#$^many . symbols"),
19994 &[
19995 "symbols",
19996 " symbols",
19997 ". symbols",
19998 " . symbols",
19999 " . symbols",
20000 " . symbols",
20001 "many . symbols",
20002 "^many . symbols",
20003 "$^many . symbols",
20004 "#$^many . symbols",
20005 "@#$^many . symbols",
20006 "!@#$^many . symbols",
20007 "is!@#$^many . symbols",
20008 "@is!@#$^many . symbols",
20009 "this@is!@#$^many . symbols",
20010 ],
20011 );
20012 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
20013}
20014
20015#[gpui::test]
20016async fn test_move_to_syntax_node_relative_jumps(tcx: &mut TestAppContext) {
20017 init_test(tcx, |_| {});
20018
20019 let mut cx = EditorLspTestContext::new(
20020 Arc::into_inner(markdown_lang()).unwrap(),
20021 Default::default(),
20022 tcx,
20023 )
20024 .await;
20025
20026 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
20027 let _state_context = cx.set_state(before);
20028 cx.run_until_parked();
20029 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
20030 .await
20031 .unwrap();
20032 cx.run_until_parked();
20033 cx.assert_editor_state(after);
20034 }
20035
20036 const ABOVE: i8 = -1;
20037 const BELOW: i8 = 1;
20038
20039 assert(
20040 ABOVE,
20041 indoc! {"
20042 # Foo
20043
20044 ˇFoo foo foo
20045
20046 # Bar
20047
20048 Bar bar bar
20049 "},
20050 indoc! {"
20051 ˇ# Foo
20052
20053 Foo foo foo
20054
20055 # Bar
20056
20057 Bar bar bar
20058 "},
20059 &mut cx,
20060 )
20061 .await;
20062
20063 assert(
20064 ABOVE,
20065 indoc! {"
20066 ˇ# Foo
20067
20068 Foo foo foo
20069
20070 # Bar
20071
20072 Bar bar bar
20073 "},
20074 indoc! {"
20075 ˇ# Foo
20076
20077 Foo foo foo
20078
20079 # Bar
20080
20081 Bar bar bar
20082 "},
20083 &mut cx,
20084 )
20085 .await;
20086
20087 assert(
20088 BELOW,
20089 indoc! {"
20090 ˇ# Foo
20091
20092 Foo foo foo
20093
20094 # Bar
20095
20096 Bar bar bar
20097 "},
20098 indoc! {"
20099 # Foo
20100
20101 Foo foo foo
20102
20103 ˇ# Bar
20104
20105 Bar bar bar
20106 "},
20107 &mut cx,
20108 )
20109 .await;
20110
20111 assert(
20112 BELOW,
20113 indoc! {"
20114 # Foo
20115
20116 ˇFoo foo foo
20117
20118 # Bar
20119
20120 Bar bar bar
20121 "},
20122 indoc! {"
20123 # Foo
20124
20125 Foo foo foo
20126
20127 ˇ# Bar
20128
20129 Bar bar bar
20130 "},
20131 &mut cx,
20132 )
20133 .await;
20134
20135 assert(
20136 BELOW,
20137 indoc! {"
20138 # Foo
20139
20140 Foo foo foo
20141
20142 ˇ# Bar
20143
20144 Bar bar bar
20145 "},
20146 indoc! {"
20147 # Foo
20148
20149 Foo foo foo
20150
20151 ˇ# Bar
20152
20153 Bar bar bar
20154 "},
20155 &mut cx,
20156 )
20157 .await;
20158
20159 assert(
20160 BELOW,
20161 indoc! {"
20162 # Foo
20163
20164 Foo foo foo
20165
20166 # Bar
20167 ˇ
20168 Bar bar bar
20169 "},
20170 indoc! {"
20171 # Foo
20172
20173 Foo foo foo
20174
20175 # Bar
20176 ˇ
20177 Bar bar bar
20178 "},
20179 &mut cx,
20180 )
20181 .await;
20182}
20183
20184#[gpui::test]
20185async fn test_move_to_syntax_node_relative_dead_zone(tcx: &mut TestAppContext) {
20186 init_test(tcx, |_| {});
20187
20188 let mut cx = EditorLspTestContext::new(
20189 Arc::into_inner(rust_lang()).unwrap(),
20190 Default::default(),
20191 tcx,
20192 )
20193 .await;
20194
20195 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
20196 let _state_context = cx.set_state(before);
20197 cx.run_until_parked();
20198 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
20199 .await
20200 .unwrap();
20201 cx.run_until_parked();
20202 cx.assert_editor_state(after);
20203 }
20204
20205 const ABOVE: i8 = -1;
20206 const BELOW: i8 = 1;
20207
20208 assert(
20209 ABOVE,
20210 indoc! {"
20211 fn foo() {
20212 // foo fn
20213 }
20214
20215 ˇ// this zone is not inside any top level outline node
20216
20217 fn bar() {
20218 // bar fn
20219 let _ = 2;
20220 }
20221 "},
20222 indoc! {"
20223 ˇfn foo() {
20224 // foo fn
20225 }
20226
20227 // this zone is not inside any top level outline node
20228
20229 fn bar() {
20230 // bar fn
20231 let _ = 2;
20232 }
20233 "},
20234 &mut cx,
20235 )
20236 .await;
20237
20238 assert(
20239 BELOW,
20240 indoc! {"
20241 fn foo() {
20242 // foo fn
20243 }
20244
20245 ˇ// this zone is not inside any top level outline node
20246
20247 fn bar() {
20248 // bar fn
20249 let _ = 2;
20250 }
20251 "},
20252 indoc! {"
20253 fn foo() {
20254 // foo fn
20255 }
20256
20257 // this zone is not inside any top level outline node
20258
20259 ˇfn bar() {
20260 // bar fn
20261 let _ = 2;
20262 }
20263 "},
20264 &mut cx,
20265 )
20266 .await;
20267}
20268
20269#[gpui::test]
20270async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
20271 init_test(cx, |_| {});
20272
20273 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
20274
20275 #[track_caller]
20276 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
20277 let _state_context = cx.set_state(before);
20278 cx.run_until_parked();
20279 cx.update_editor(|editor, window, cx| {
20280 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
20281 });
20282 cx.run_until_parked();
20283 cx.assert_editor_state(after);
20284 }
20285
20286 // Outside bracket jumps to outside of matching bracket
20287 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
20288 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
20289
20290 // Inside bracket jumps to inside of matching bracket
20291 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
20292 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
20293
20294 // When outside a bracket and inside, favor jumping to the inside bracket
20295 assert(
20296 "console.log('foo', [1, 2, 3]ˇ);",
20297 "console.log('foo', ˇ[1, 2, 3]);",
20298 &mut cx,
20299 );
20300 assert(
20301 "console.log(ˇ'foo', [1, 2, 3]);",
20302 "console.log('foo'ˇ, [1, 2, 3]);",
20303 &mut cx,
20304 );
20305
20306 // Bias forward if two options are equally likely
20307 assert(
20308 "let result = curried_fun()ˇ();",
20309 "let result = curried_fun()()ˇ;",
20310 &mut cx,
20311 );
20312
20313 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
20314 assert(
20315 indoc! {"
20316 function test() {
20317 console.log('test')ˇ
20318 }"},
20319 indoc! {"
20320 function test() {
20321 console.logˇ('test')
20322 }"},
20323 &mut cx,
20324 );
20325}
20326
20327#[gpui::test]
20328async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
20329 init_test(cx, |_| {});
20330 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
20331 language_registry.add(markdown_lang());
20332 language_registry.add(rust_lang());
20333 let buffer = cx.new(|cx| {
20334 let mut buffer = language::Buffer::local(
20335 indoc! {"
20336 ```rs
20337 impl Worktree {
20338 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20339 }
20340 }
20341 ```
20342 "},
20343 cx,
20344 );
20345 buffer.set_language_registry(language_registry.clone());
20346 buffer.set_language(Some(markdown_lang()), cx);
20347 buffer
20348 });
20349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20350 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
20351 cx.executor().run_until_parked();
20352 _ = editor.update(cx, |editor, window, cx| {
20353 // Case 1: Test outer enclosing brackets
20354 select_ranges(
20355 editor,
20356 &indoc! {"
20357 ```rs
20358 impl Worktree {
20359 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20360 }
20361 }ˇ
20362 ```
20363 "},
20364 window,
20365 cx,
20366 );
20367 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
20368 assert_text_with_selections(
20369 editor,
20370 &indoc! {"
20371 ```rs
20372 impl Worktree ˇ{
20373 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20374 }
20375 }
20376 ```
20377 "},
20378 cx,
20379 );
20380 // Case 2: Test inner enclosing brackets
20381 select_ranges(
20382 editor,
20383 &indoc! {"
20384 ```rs
20385 impl Worktree {
20386 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20387 }ˇ
20388 }
20389 ```
20390 "},
20391 window,
20392 cx,
20393 );
20394 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
20395 assert_text_with_selections(
20396 editor,
20397 &indoc! {"
20398 ```rs
20399 impl Worktree {
20400 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
20401 }
20402 }
20403 ```
20404 "},
20405 cx,
20406 );
20407 });
20408}
20409
20410#[gpui::test]
20411async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
20412 init_test(cx, |_| {});
20413
20414 let fs = FakeFs::new(cx.executor());
20415 fs.insert_tree(
20416 path!("/a"),
20417 json!({
20418 "main.rs": "fn main() { let a = 5; }",
20419 "other.rs": "// Test file",
20420 }),
20421 )
20422 .await;
20423 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20424
20425 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20426 language_registry.add(Arc::new(Language::new(
20427 LanguageConfig {
20428 name: "Rust".into(),
20429 matcher: LanguageMatcher {
20430 path_suffixes: vec!["rs".to_string()],
20431 ..Default::default()
20432 },
20433 brackets: BracketPairConfig {
20434 pairs: vec![BracketPair {
20435 start: "{".to_string(),
20436 end: "}".to_string(),
20437 close: true,
20438 surround: true,
20439 newline: true,
20440 }],
20441 disabled_scopes_by_bracket_ix: Vec::new(),
20442 },
20443 ..Default::default()
20444 },
20445 Some(tree_sitter_rust::LANGUAGE.into()),
20446 )));
20447 let mut fake_servers = language_registry.register_fake_lsp(
20448 "Rust",
20449 FakeLspAdapter {
20450 capabilities: lsp::ServerCapabilities {
20451 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
20452 first_trigger_character: "{".to_string(),
20453 more_trigger_character: None,
20454 }),
20455 ..Default::default()
20456 },
20457 ..Default::default()
20458 },
20459 );
20460
20461 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
20462 let workspace = window
20463 .read_with(cx, |mw, _| mw.workspace().clone())
20464 .unwrap();
20465
20466 let cx = &mut VisualTestContext::from_window(*window, cx);
20467
20468 let worktree_id = workspace.update_in(cx, |workspace, _, cx| {
20469 workspace.project().update(cx, |project, cx| {
20470 project.worktrees(cx).next().unwrap().read(cx).id()
20471 })
20472 });
20473
20474 let buffer = project
20475 .update(cx, |project, cx| {
20476 project.open_local_buffer(path!("/a/main.rs"), cx)
20477 })
20478 .await
20479 .unwrap();
20480 let editor_handle = workspace
20481 .update_in(cx, |workspace, window, cx| {
20482 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
20483 })
20484 .await
20485 .unwrap()
20486 .downcast::<Editor>()
20487 .unwrap();
20488
20489 let fake_server = fake_servers.next().await.unwrap();
20490
20491 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
20492 |params, _| async move {
20493 assert_eq!(
20494 params.text_document_position.text_document.uri,
20495 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
20496 );
20497 assert_eq!(
20498 params.text_document_position.position,
20499 lsp::Position::new(0, 21),
20500 );
20501
20502 Ok(Some(vec![lsp::TextEdit {
20503 new_text: "]".to_string(),
20504 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20505 }]))
20506 },
20507 );
20508
20509 editor_handle.update_in(cx, |editor, window, cx| {
20510 window.focus(&editor.focus_handle(cx), cx);
20511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20512 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
20513 });
20514 editor.handle_input("{", window, cx);
20515 });
20516
20517 cx.executor().run_until_parked();
20518
20519 buffer.update(cx, |buffer, _| {
20520 assert_eq!(
20521 buffer.text(),
20522 "fn main() { let a = {5}; }",
20523 "No extra braces from on type formatting should appear in the buffer"
20524 )
20525 });
20526}
20527
20528#[gpui::test(iterations = 20, seeds(31))]
20529async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
20530 init_test(cx, |_| {});
20531
20532 let mut cx = EditorLspTestContext::new_rust(
20533 lsp::ServerCapabilities {
20534 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
20535 first_trigger_character: ".".to_string(),
20536 more_trigger_character: None,
20537 }),
20538 ..Default::default()
20539 },
20540 cx,
20541 )
20542 .await;
20543
20544 cx.update_buffer(|buffer, _| {
20545 // This causes autoindent to be async.
20546 buffer.set_sync_parse_timeout(None)
20547 });
20548
20549 cx.set_state("fn c() {\n d()ˇ\n}\n");
20550 cx.simulate_keystroke("\n");
20551 cx.run_until_parked();
20552
20553 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
20554 let mut request =
20555 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
20556 let buffer_cloned = buffer_cloned.clone();
20557 async move {
20558 buffer_cloned.update(&mut cx, |buffer, _| {
20559 assert_eq!(
20560 buffer.text(),
20561 "fn c() {\n d()\n .\n}\n",
20562 "OnTypeFormatting should triggered after autoindent applied"
20563 )
20564 });
20565
20566 Ok(Some(vec![]))
20567 }
20568 });
20569
20570 cx.simulate_keystroke(".");
20571 cx.run_until_parked();
20572
20573 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
20574 assert!(request.next().await.is_some());
20575 request.close();
20576 assert!(request.next().await.is_none());
20577}
20578
20579#[gpui::test]
20580async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
20581 init_test(cx, |_| {});
20582
20583 let fs = FakeFs::new(cx.executor());
20584 fs.insert_tree(
20585 path!("/a"),
20586 json!({
20587 "main.rs": "fn main() { let a = 5; }",
20588 "other.rs": "// Test file",
20589 }),
20590 )
20591 .await;
20592
20593 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20594
20595 let server_restarts = Arc::new(AtomicUsize::new(0));
20596 let closure_restarts = Arc::clone(&server_restarts);
20597 let language_server_name = "test language server";
20598 let language_name: LanguageName = "Rust".into();
20599
20600 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20601 language_registry.add(Arc::new(Language::new(
20602 LanguageConfig {
20603 name: language_name.clone(),
20604 matcher: LanguageMatcher {
20605 path_suffixes: vec!["rs".to_string()],
20606 ..Default::default()
20607 },
20608 ..Default::default()
20609 },
20610 Some(tree_sitter_rust::LANGUAGE.into()),
20611 )));
20612 let mut fake_servers = language_registry.register_fake_lsp(
20613 "Rust",
20614 FakeLspAdapter {
20615 name: language_server_name,
20616 initialization_options: Some(json!({
20617 "testOptionValue": true
20618 })),
20619 initializer: Some(Box::new(move |fake_server| {
20620 let task_restarts = Arc::clone(&closure_restarts);
20621 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
20622 task_restarts.fetch_add(1, atomic::Ordering::Release);
20623 futures::future::ready(Ok(()))
20624 });
20625 })),
20626 ..Default::default()
20627 },
20628 );
20629
20630 let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
20631 let _buffer = project
20632 .update(cx, |project, cx| {
20633 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
20634 })
20635 .await
20636 .unwrap();
20637 let _fake_server = fake_servers.next().await.unwrap();
20638 update_test_language_settings(cx, &|language_settings| {
20639 language_settings.languages.0.insert(
20640 language_name.clone().0.to_string(),
20641 LanguageSettingsContent {
20642 tab_size: NonZeroU32::new(8),
20643 ..Default::default()
20644 },
20645 );
20646 });
20647 cx.executor().run_until_parked();
20648 assert_eq!(
20649 server_restarts.load(atomic::Ordering::Acquire),
20650 0,
20651 "Should not restart LSP server on an unrelated change"
20652 );
20653
20654 update_test_project_settings(cx, &|project_settings| {
20655 project_settings.lsp.0.insert(
20656 "Some other server name".into(),
20657 LspSettings {
20658 binary: None,
20659 settings: None,
20660 initialization_options: Some(json!({
20661 "some other init value": false
20662 })),
20663 enable_lsp_tasks: false,
20664 fetch: None,
20665 },
20666 );
20667 });
20668 cx.executor().run_until_parked();
20669 assert_eq!(
20670 server_restarts.load(atomic::Ordering::Acquire),
20671 0,
20672 "Should not restart LSP server on an unrelated LSP settings change"
20673 );
20674
20675 update_test_project_settings(cx, &|project_settings| {
20676 project_settings.lsp.0.insert(
20677 language_server_name.into(),
20678 LspSettings {
20679 binary: None,
20680 settings: None,
20681 initialization_options: Some(json!({
20682 "anotherInitValue": false
20683 })),
20684 enable_lsp_tasks: false,
20685 fetch: None,
20686 },
20687 );
20688 });
20689 cx.executor().run_until_parked();
20690 assert_eq!(
20691 server_restarts.load(atomic::Ordering::Acquire),
20692 1,
20693 "Should restart LSP server on a related LSP settings change"
20694 );
20695
20696 update_test_project_settings(cx, &|project_settings| {
20697 project_settings.lsp.0.insert(
20698 language_server_name.into(),
20699 LspSettings {
20700 binary: None,
20701 settings: None,
20702 initialization_options: Some(json!({
20703 "anotherInitValue": false
20704 })),
20705 enable_lsp_tasks: false,
20706 fetch: None,
20707 },
20708 );
20709 });
20710 cx.executor().run_until_parked();
20711 assert_eq!(
20712 server_restarts.load(atomic::Ordering::Acquire),
20713 1,
20714 "Should not restart LSP server on a related LSP settings change that is the same"
20715 );
20716
20717 update_test_project_settings(cx, &|project_settings| {
20718 project_settings.lsp.0.insert(
20719 language_server_name.into(),
20720 LspSettings {
20721 binary: None,
20722 settings: None,
20723 initialization_options: None,
20724 enable_lsp_tasks: false,
20725 fetch: None,
20726 },
20727 );
20728 });
20729 cx.executor().run_until_parked();
20730 assert_eq!(
20731 server_restarts.load(atomic::Ordering::Acquire),
20732 2,
20733 "Should restart LSP server on another related LSP settings change"
20734 );
20735}
20736
20737#[gpui::test]
20738async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
20739 init_test(cx, |_| {});
20740
20741 let mut cx = EditorLspTestContext::new_rust(
20742 lsp::ServerCapabilities {
20743 completion_provider: Some(lsp::CompletionOptions {
20744 trigger_characters: Some(vec![".".to_string()]),
20745 resolve_provider: Some(true),
20746 ..Default::default()
20747 }),
20748 ..Default::default()
20749 },
20750 cx,
20751 )
20752 .await;
20753
20754 cx.set_state("fn main() { let a = 2ˇ; }");
20755 cx.simulate_keystroke(".");
20756 let completion_item = lsp::CompletionItem {
20757 label: "some".into(),
20758 kind: Some(lsp::CompletionItemKind::SNIPPET),
20759 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
20760 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
20761 kind: lsp::MarkupKind::Markdown,
20762 value: "```rust\nSome(2)\n```".to_string(),
20763 })),
20764 deprecated: Some(false),
20765 sort_text: Some("fffffff2".to_string()),
20766 filter_text: Some("some".to_string()),
20767 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
20768 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20769 range: lsp::Range {
20770 start: lsp::Position {
20771 line: 0,
20772 character: 22,
20773 },
20774 end: lsp::Position {
20775 line: 0,
20776 character: 22,
20777 },
20778 },
20779 new_text: "Some(2)".to_string(),
20780 })),
20781 additional_text_edits: Some(vec![lsp::TextEdit {
20782 range: lsp::Range {
20783 start: lsp::Position {
20784 line: 0,
20785 character: 20,
20786 },
20787 end: lsp::Position {
20788 line: 0,
20789 character: 22,
20790 },
20791 },
20792 new_text: "".to_string(),
20793 }]),
20794 ..Default::default()
20795 };
20796
20797 let closure_completion_item = completion_item.clone();
20798 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20799 let task_completion_item = closure_completion_item.clone();
20800 async move {
20801 Ok(Some(lsp::CompletionResponse::Array(vec![
20802 task_completion_item,
20803 ])))
20804 }
20805 });
20806
20807 request.next().await;
20808
20809 cx.condition(|editor, _| editor.context_menu_visible())
20810 .await;
20811 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20812 editor
20813 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20814 .unwrap()
20815 });
20816 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
20817
20818 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20819 let task_completion_item = completion_item.clone();
20820 async move { Ok(task_completion_item) }
20821 })
20822 .next()
20823 .await
20824 .unwrap();
20825 apply_additional_edits.await.unwrap();
20826 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
20827}
20828
20829#[gpui::test]
20830async fn test_completions_with_additional_edits_undo(cx: &mut TestAppContext) {
20831 init_test(cx, |_| {});
20832
20833 let mut cx = EditorLspTestContext::new_rust(
20834 lsp::ServerCapabilities {
20835 completion_provider: Some(lsp::CompletionOptions {
20836 trigger_characters: Some(vec![".".to_string()]),
20837 resolve_provider: Some(true),
20838 ..Default::default()
20839 }),
20840 ..Default::default()
20841 },
20842 cx,
20843 )
20844 .await;
20845
20846 cx.set_state("fn main() { let a = 2ˇ; }");
20847 cx.simulate_keystroke(".");
20848 let completion_item = lsp::CompletionItem {
20849 label: "some".into(),
20850 kind: Some(lsp::CompletionItemKind::SNIPPET),
20851 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
20852 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
20853 kind: lsp::MarkupKind::Markdown,
20854 value: "```rust\nSome(2)\n```".to_string(),
20855 })),
20856 deprecated: Some(false),
20857 sort_text: Some("fffffff2".to_string()),
20858 filter_text: Some("some".to_string()),
20859 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
20860 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20861 range: lsp::Range {
20862 start: lsp::Position {
20863 line: 0,
20864 character: 22,
20865 },
20866 end: lsp::Position {
20867 line: 0,
20868 character: 22,
20869 },
20870 },
20871 new_text: "Some(2)".to_string(),
20872 })),
20873 additional_text_edits: Some(vec![lsp::TextEdit {
20874 range: lsp::Range {
20875 start: lsp::Position {
20876 line: 0,
20877 character: 20,
20878 },
20879 end: lsp::Position {
20880 line: 0,
20881 character: 22,
20882 },
20883 },
20884 new_text: "".to_string(),
20885 }]),
20886 ..Default::default()
20887 };
20888
20889 let closure_completion_item = completion_item.clone();
20890 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20891 let task_completion_item = closure_completion_item.clone();
20892 async move {
20893 Ok(Some(lsp::CompletionResponse::Array(vec![
20894 task_completion_item,
20895 ])))
20896 }
20897 });
20898
20899 request.next().await;
20900
20901 cx.condition(|editor, _| editor.context_menu_visible())
20902 .await;
20903 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20904 editor
20905 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20906 .unwrap()
20907 });
20908 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
20909
20910 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20911 let task_completion_item = completion_item.clone();
20912 async move { Ok(task_completion_item) }
20913 })
20914 .next()
20915 .await
20916 .unwrap();
20917 apply_additional_edits.await.unwrap();
20918 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
20919
20920 cx.update_editor(|editor, window, cx| {
20921 editor.undo(&crate::Undo, window, cx);
20922 });
20923 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
20924}
20925
20926#[gpui::test]
20927async fn test_completions_with_additional_edits_and_multiple_cursors(cx: &mut TestAppContext) {
20928 init_test(cx, |_| {});
20929
20930 let mut cx = EditorLspTestContext::new_typescript(
20931 lsp::ServerCapabilities {
20932 completion_provider: Some(lsp::CompletionOptions {
20933 resolve_provider: Some(true),
20934 ..Default::default()
20935 }),
20936 ..Default::default()
20937 },
20938 cx,
20939 )
20940 .await;
20941
20942 cx.set_state(
20943 "import { «Fooˇ» } from './types';\n\nclass Bar {\n method(): «Fooˇ» { return new Foo(); }\n}",
20944 );
20945
20946 cx.simulate_keystroke("F");
20947 cx.simulate_keystroke("o");
20948
20949 let completion_item = lsp::CompletionItem {
20950 label: "FooBar".into(),
20951 kind: Some(lsp::CompletionItemKind::CLASS),
20952 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20953 range: lsp::Range {
20954 start: lsp::Position {
20955 line: 3,
20956 character: 14,
20957 },
20958 end: lsp::Position {
20959 line: 3,
20960 character: 16,
20961 },
20962 },
20963 new_text: "FooBar".to_string(),
20964 })),
20965 additional_text_edits: Some(vec![lsp::TextEdit {
20966 range: lsp::Range {
20967 start: lsp::Position {
20968 line: 0,
20969 character: 9,
20970 },
20971 end: lsp::Position {
20972 line: 0,
20973 character: 11,
20974 },
20975 },
20976 new_text: "FooBar".to_string(),
20977 }]),
20978 ..Default::default()
20979 };
20980
20981 let closure_completion_item = completion_item.clone();
20982 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20983 let task_completion_item = closure_completion_item.clone();
20984 async move {
20985 Ok(Some(lsp::CompletionResponse::Array(vec![
20986 task_completion_item,
20987 ])))
20988 }
20989 });
20990
20991 request.next().await;
20992
20993 cx.condition(|editor, _| editor.context_menu_visible())
20994 .await;
20995 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20996 editor
20997 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20998 .unwrap()
20999 });
21000
21001 cx.assert_editor_state(
21002 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
21003 );
21004
21005 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21006 let task_completion_item = completion_item.clone();
21007 async move { Ok(task_completion_item) }
21008 })
21009 .next()
21010 .await
21011 .unwrap();
21012
21013 apply_additional_edits.await.unwrap();
21014
21015 cx.assert_editor_state(
21016 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
21017 );
21018}
21019
21020#[gpui::test]
21021async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
21022 init_test(cx, |_| {});
21023
21024 let mut cx = EditorLspTestContext::new_rust(
21025 lsp::ServerCapabilities {
21026 completion_provider: Some(lsp::CompletionOptions {
21027 trigger_characters: Some(vec![".".to_string()]),
21028 resolve_provider: Some(true),
21029 ..Default::default()
21030 }),
21031 ..Default::default()
21032 },
21033 cx,
21034 )
21035 .await;
21036
21037 cx.set_state("fn main() { let a = 2ˇ; }");
21038 cx.simulate_keystroke(".");
21039
21040 let item1 = lsp::CompletionItem {
21041 label: "method id()".to_string(),
21042 filter_text: Some("id".to_string()),
21043 detail: None,
21044 documentation: None,
21045 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21046 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21047 new_text: ".id".to_string(),
21048 })),
21049 ..lsp::CompletionItem::default()
21050 };
21051
21052 let item2 = lsp::CompletionItem {
21053 label: "other".to_string(),
21054 filter_text: Some("other".to_string()),
21055 detail: None,
21056 documentation: None,
21057 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21058 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21059 new_text: ".other".to_string(),
21060 })),
21061 ..lsp::CompletionItem::default()
21062 };
21063
21064 let item1 = item1.clone();
21065 cx.set_request_handler::<lsp::request::Completion, _, _>({
21066 let item1 = item1.clone();
21067 move |_, _, _| {
21068 let item1 = item1.clone();
21069 let item2 = item2.clone();
21070 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
21071 }
21072 })
21073 .next()
21074 .await;
21075
21076 cx.condition(|editor, _| editor.context_menu_visible())
21077 .await;
21078 cx.update_editor(|editor, _, _| {
21079 let context_menu = editor.context_menu.borrow_mut();
21080 let context_menu = context_menu
21081 .as_ref()
21082 .expect("Should have the context menu deployed");
21083 match context_menu {
21084 CodeContextMenu::Completions(completions_menu) => {
21085 let completions = completions_menu.completions.borrow_mut();
21086 assert_eq!(
21087 completions
21088 .iter()
21089 .map(|completion| &completion.label.text)
21090 .collect::<Vec<_>>(),
21091 vec!["method id()", "other"]
21092 )
21093 }
21094 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21095 }
21096 });
21097
21098 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
21099 let item1 = item1.clone();
21100 move |_, item_to_resolve, _| {
21101 let item1 = item1.clone();
21102 async move {
21103 if item1 == item_to_resolve {
21104 Ok(lsp::CompletionItem {
21105 label: "method id()".to_string(),
21106 filter_text: Some("id".to_string()),
21107 detail: Some("Now resolved!".to_string()),
21108 documentation: Some(lsp::Documentation::String("Docs".to_string())),
21109 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21110 range: lsp::Range::new(
21111 lsp::Position::new(0, 22),
21112 lsp::Position::new(0, 22),
21113 ),
21114 new_text: ".id".to_string(),
21115 })),
21116 ..lsp::CompletionItem::default()
21117 })
21118 } else {
21119 Ok(item_to_resolve)
21120 }
21121 }
21122 }
21123 })
21124 .next()
21125 .await
21126 .unwrap();
21127 cx.run_until_parked();
21128
21129 cx.update_editor(|editor, window, cx| {
21130 editor.context_menu_next(&Default::default(), window, cx);
21131 });
21132 cx.run_until_parked();
21133
21134 cx.update_editor(|editor, _, _| {
21135 let context_menu = editor.context_menu.borrow_mut();
21136 let context_menu = context_menu
21137 .as_ref()
21138 .expect("Should have the context menu deployed");
21139 match context_menu {
21140 CodeContextMenu::Completions(completions_menu) => {
21141 let completions = completions_menu.completions.borrow_mut();
21142 assert_eq!(
21143 completions
21144 .iter()
21145 .map(|completion| &completion.label.text)
21146 .collect::<Vec<_>>(),
21147 vec!["method id() Now resolved!", "other"],
21148 "Should update first completion label, but not second as the filter text did not match."
21149 );
21150 }
21151 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21152 }
21153 });
21154}
21155
21156#[gpui::test]
21157async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
21158 init_test(cx, |_| {});
21159 let mut cx = EditorLspTestContext::new_rust(
21160 lsp::ServerCapabilities {
21161 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
21162 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
21163 completion_provider: Some(lsp::CompletionOptions {
21164 resolve_provider: Some(true),
21165 ..Default::default()
21166 }),
21167 ..Default::default()
21168 },
21169 cx,
21170 )
21171 .await;
21172 cx.set_state(indoc! {"
21173 struct TestStruct {
21174 field: i32
21175 }
21176
21177 fn mainˇ() {
21178 let unused_var = 42;
21179 let test_struct = TestStruct { field: 42 };
21180 }
21181 "});
21182 let symbol_range = cx.lsp_range(indoc! {"
21183 struct TestStruct {
21184 field: i32
21185 }
21186
21187 «fn main»() {
21188 let unused_var = 42;
21189 let test_struct = TestStruct { field: 42 };
21190 }
21191 "});
21192 let mut hover_requests =
21193 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
21194 Ok(Some(lsp::Hover {
21195 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
21196 kind: lsp::MarkupKind::Markdown,
21197 value: "Function documentation".to_string(),
21198 }),
21199 range: Some(symbol_range),
21200 }))
21201 });
21202
21203 // Case 1: Test that code action menu hide hover popover
21204 cx.dispatch_action(Hover);
21205 hover_requests.next().await;
21206 cx.condition(|editor, _| editor.hover_state.visible()).await;
21207 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
21208 move |_, _, _| async move {
21209 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
21210 lsp::CodeAction {
21211 title: "Remove unused variable".to_string(),
21212 kind: Some(CodeActionKind::QUICKFIX),
21213 edit: Some(lsp::WorkspaceEdit {
21214 changes: Some(
21215 [(
21216 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
21217 vec![lsp::TextEdit {
21218 range: lsp::Range::new(
21219 lsp::Position::new(5, 4),
21220 lsp::Position::new(5, 27),
21221 ),
21222 new_text: "".to_string(),
21223 }],
21224 )]
21225 .into_iter()
21226 .collect(),
21227 ),
21228 ..Default::default()
21229 }),
21230 ..Default::default()
21231 },
21232 )]))
21233 },
21234 );
21235 cx.update_editor(|editor, window, cx| {
21236 editor.toggle_code_actions(
21237 &ToggleCodeActions {
21238 deployed_from: None,
21239 quick_launch: false,
21240 },
21241 window,
21242 cx,
21243 );
21244 });
21245 code_action_requests.next().await;
21246 cx.run_until_parked();
21247 cx.condition(|editor, _| editor.context_menu_visible())
21248 .await;
21249 cx.update_editor(|editor, _, _| {
21250 assert!(
21251 !editor.hover_state.visible(),
21252 "Hover popover should be hidden when code action menu is shown"
21253 );
21254 // Hide code actions
21255 editor.context_menu.take();
21256 });
21257
21258 // Case 2: Test that code completions hide hover popover
21259 cx.dispatch_action(Hover);
21260 hover_requests.next().await;
21261 cx.condition(|editor, _| editor.hover_state.visible()).await;
21262 let counter = Arc::new(AtomicUsize::new(0));
21263 let mut completion_requests =
21264 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21265 let counter = counter.clone();
21266 async move {
21267 counter.fetch_add(1, atomic::Ordering::Release);
21268 Ok(Some(lsp::CompletionResponse::Array(vec![
21269 lsp::CompletionItem {
21270 label: "main".into(),
21271 kind: Some(lsp::CompletionItemKind::FUNCTION),
21272 detail: Some("() -> ()".to_string()),
21273 ..Default::default()
21274 },
21275 lsp::CompletionItem {
21276 label: "TestStruct".into(),
21277 kind: Some(lsp::CompletionItemKind::STRUCT),
21278 detail: Some("struct TestStruct".to_string()),
21279 ..Default::default()
21280 },
21281 ])))
21282 }
21283 });
21284 cx.update_editor(|editor, window, cx| {
21285 editor.show_completions(&ShowCompletions, window, cx);
21286 });
21287 completion_requests.next().await;
21288 cx.condition(|editor, _| editor.context_menu_visible())
21289 .await;
21290 cx.update_editor(|editor, _, _| {
21291 assert!(
21292 !editor.hover_state.visible(),
21293 "Hover popover should be hidden when completion menu is shown"
21294 );
21295 });
21296}
21297
21298#[gpui::test]
21299async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
21300 init_test(cx, |_| {});
21301
21302 let mut cx = EditorLspTestContext::new_rust(
21303 lsp::ServerCapabilities {
21304 completion_provider: Some(lsp::CompletionOptions {
21305 trigger_characters: Some(vec![".".to_string()]),
21306 resolve_provider: Some(true),
21307 ..Default::default()
21308 }),
21309 ..Default::default()
21310 },
21311 cx,
21312 )
21313 .await;
21314
21315 cx.set_state("fn main() { let a = 2ˇ; }");
21316 cx.simulate_keystroke(".");
21317
21318 let unresolved_item_1 = lsp::CompletionItem {
21319 label: "id".to_string(),
21320 filter_text: Some("id".to_string()),
21321 detail: None,
21322 documentation: None,
21323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21324 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21325 new_text: ".id".to_string(),
21326 })),
21327 ..lsp::CompletionItem::default()
21328 };
21329 let resolved_item_1 = lsp::CompletionItem {
21330 additional_text_edits: Some(vec![lsp::TextEdit {
21331 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
21332 new_text: "!!".to_string(),
21333 }]),
21334 ..unresolved_item_1.clone()
21335 };
21336 let unresolved_item_2 = lsp::CompletionItem {
21337 label: "other".to_string(),
21338 filter_text: Some("other".to_string()),
21339 detail: None,
21340 documentation: None,
21341 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21342 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21343 new_text: ".other".to_string(),
21344 })),
21345 ..lsp::CompletionItem::default()
21346 };
21347 let resolved_item_2 = lsp::CompletionItem {
21348 additional_text_edits: Some(vec![lsp::TextEdit {
21349 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
21350 new_text: "??".to_string(),
21351 }]),
21352 ..unresolved_item_2.clone()
21353 };
21354
21355 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
21356 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
21357 cx.lsp
21358 .server
21359 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
21360 let unresolved_item_1 = unresolved_item_1.clone();
21361 let resolved_item_1 = resolved_item_1.clone();
21362 let unresolved_item_2 = unresolved_item_2.clone();
21363 let resolved_item_2 = resolved_item_2.clone();
21364 let resolve_requests_1 = resolve_requests_1.clone();
21365 let resolve_requests_2 = resolve_requests_2.clone();
21366 move |unresolved_request, _| {
21367 let unresolved_item_1 = unresolved_item_1.clone();
21368 let resolved_item_1 = resolved_item_1.clone();
21369 let unresolved_item_2 = unresolved_item_2.clone();
21370 let resolved_item_2 = resolved_item_2.clone();
21371 let resolve_requests_1 = resolve_requests_1.clone();
21372 let resolve_requests_2 = resolve_requests_2.clone();
21373 async move {
21374 if unresolved_request == unresolved_item_1 {
21375 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
21376 Ok(resolved_item_1.clone())
21377 } else if unresolved_request == unresolved_item_2 {
21378 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
21379 Ok(resolved_item_2.clone())
21380 } else {
21381 panic!("Unexpected completion item {unresolved_request:?}")
21382 }
21383 }
21384 }
21385 })
21386 .detach();
21387
21388 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21389 let unresolved_item_1 = unresolved_item_1.clone();
21390 let unresolved_item_2 = unresolved_item_2.clone();
21391 async move {
21392 Ok(Some(lsp::CompletionResponse::Array(vec![
21393 unresolved_item_1,
21394 unresolved_item_2,
21395 ])))
21396 }
21397 })
21398 .next()
21399 .await;
21400
21401 cx.condition(|editor, _| editor.context_menu_visible())
21402 .await;
21403 cx.update_editor(|editor, _, _| {
21404 let context_menu = editor.context_menu.borrow_mut();
21405 let context_menu = context_menu
21406 .as_ref()
21407 .expect("Should have the context menu deployed");
21408 match context_menu {
21409 CodeContextMenu::Completions(completions_menu) => {
21410 let completions = completions_menu.completions.borrow_mut();
21411 assert_eq!(
21412 completions
21413 .iter()
21414 .map(|completion| &completion.label.text)
21415 .collect::<Vec<_>>(),
21416 vec!["id", "other"]
21417 )
21418 }
21419 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21420 }
21421 });
21422 cx.run_until_parked();
21423
21424 cx.update_editor(|editor, window, cx| {
21425 editor.context_menu_next(&ContextMenuNext, window, cx);
21426 });
21427 cx.run_until_parked();
21428 cx.update_editor(|editor, window, cx| {
21429 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
21430 });
21431 cx.run_until_parked();
21432 cx.update_editor(|editor, window, cx| {
21433 editor.context_menu_next(&ContextMenuNext, window, cx);
21434 });
21435 cx.run_until_parked();
21436 cx.update_editor(|editor, window, cx| {
21437 editor
21438 .compose_completion(&ComposeCompletion::default(), window, cx)
21439 .expect("No task returned")
21440 })
21441 .await
21442 .expect("Completion failed");
21443 cx.run_until_parked();
21444
21445 cx.update_editor(|editor, _, cx| {
21446 assert_eq!(
21447 resolve_requests_1.load(atomic::Ordering::Acquire),
21448 1,
21449 "Should always resolve once despite multiple selections"
21450 );
21451 assert_eq!(
21452 resolve_requests_2.load(atomic::Ordering::Acquire),
21453 1,
21454 "Should always resolve once after multiple selections and applying the completion"
21455 );
21456 assert_eq!(
21457 editor.text(cx),
21458 "fn main() { let a = ??.other; }",
21459 "Should use resolved data when applying the completion"
21460 );
21461 });
21462}
21463
21464#[gpui::test]
21465async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
21466 init_test(cx, |_| {});
21467
21468 let item_0 = lsp::CompletionItem {
21469 label: "abs".into(),
21470 insert_text: Some("abs".into()),
21471 data: Some(json!({ "very": "special"})),
21472 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
21473 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21474 lsp::InsertReplaceEdit {
21475 new_text: "abs".to_string(),
21476 insert: lsp::Range::default(),
21477 replace: lsp::Range::default(),
21478 },
21479 )),
21480 ..lsp::CompletionItem::default()
21481 };
21482 let items = iter::once(item_0.clone())
21483 .chain((11..51).map(|i| lsp::CompletionItem {
21484 label: format!("item_{}", i),
21485 insert_text: Some(format!("item_{}", i)),
21486 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
21487 ..lsp::CompletionItem::default()
21488 }))
21489 .collect::<Vec<_>>();
21490
21491 let default_commit_characters = vec!["?".to_string()];
21492 let default_data = json!({ "default": "data"});
21493 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
21494 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
21495 let default_edit_range = lsp::Range {
21496 start: lsp::Position {
21497 line: 0,
21498 character: 5,
21499 },
21500 end: lsp::Position {
21501 line: 0,
21502 character: 5,
21503 },
21504 };
21505
21506 let mut cx = EditorLspTestContext::new_rust(
21507 lsp::ServerCapabilities {
21508 completion_provider: Some(lsp::CompletionOptions {
21509 trigger_characters: Some(vec![".".to_string()]),
21510 resolve_provider: Some(true),
21511 ..Default::default()
21512 }),
21513 ..Default::default()
21514 },
21515 cx,
21516 )
21517 .await;
21518
21519 cx.set_state("fn main() { let a = 2ˇ; }");
21520 cx.simulate_keystroke(".");
21521
21522 let completion_data = default_data.clone();
21523 let completion_characters = default_commit_characters.clone();
21524 let completion_items = items.clone();
21525 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21526 let default_data = completion_data.clone();
21527 let default_commit_characters = completion_characters.clone();
21528 let items = completion_items.clone();
21529 async move {
21530 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21531 items,
21532 item_defaults: Some(lsp::CompletionListItemDefaults {
21533 data: Some(default_data.clone()),
21534 commit_characters: Some(default_commit_characters.clone()),
21535 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
21536 default_edit_range,
21537 )),
21538 insert_text_format: Some(default_insert_text_format),
21539 insert_text_mode: Some(default_insert_text_mode),
21540 }),
21541 ..lsp::CompletionList::default()
21542 })))
21543 }
21544 })
21545 .next()
21546 .await;
21547
21548 let resolved_items = Arc::new(Mutex::new(Vec::new()));
21549 cx.lsp
21550 .server
21551 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
21552 let closure_resolved_items = resolved_items.clone();
21553 move |item_to_resolve, _| {
21554 let closure_resolved_items = closure_resolved_items.clone();
21555 async move {
21556 closure_resolved_items.lock().push(item_to_resolve.clone());
21557 Ok(item_to_resolve)
21558 }
21559 }
21560 })
21561 .detach();
21562
21563 cx.condition(|editor, _| editor.context_menu_visible())
21564 .await;
21565 cx.run_until_parked();
21566 cx.update_editor(|editor, _, _| {
21567 let menu = editor.context_menu.borrow_mut();
21568 match menu.as_ref().expect("should have the completions menu") {
21569 CodeContextMenu::Completions(completions_menu) => {
21570 assert_eq!(
21571 completions_menu
21572 .entries
21573 .borrow()
21574 .iter()
21575 .map(|mat| mat.string.clone())
21576 .collect::<Vec<String>>(),
21577 items
21578 .iter()
21579 .map(|completion| completion.label.clone())
21580 .collect::<Vec<String>>()
21581 );
21582 }
21583 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
21584 }
21585 });
21586 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
21587 // with 4 from the end.
21588 assert_eq!(
21589 *resolved_items.lock(),
21590 [&items[0..16], &items[items.len() - 4..items.len()]]
21591 .concat()
21592 .iter()
21593 .cloned()
21594 .map(|mut item| {
21595 if item.data.is_none() {
21596 item.data = Some(default_data.clone());
21597 }
21598 item
21599 })
21600 .collect::<Vec<lsp::CompletionItem>>(),
21601 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
21602 );
21603 resolved_items.lock().clear();
21604
21605 cx.update_editor(|editor, window, cx| {
21606 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
21607 });
21608 cx.run_until_parked();
21609 // Completions that have already been resolved are skipped.
21610 assert_eq!(
21611 *resolved_items.lock(),
21612 items[items.len() - 17..items.len() - 4]
21613 .iter()
21614 .cloned()
21615 .map(|mut item| {
21616 if item.data.is_none() {
21617 item.data = Some(default_data.clone());
21618 }
21619 item
21620 })
21621 .collect::<Vec<lsp::CompletionItem>>()
21622 );
21623 resolved_items.lock().clear();
21624}
21625
21626#[gpui::test]
21627async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
21628 init_test(cx, |_| {});
21629
21630 let mut cx = EditorLspTestContext::new(
21631 Language::new(
21632 LanguageConfig {
21633 matcher: LanguageMatcher {
21634 path_suffixes: vec!["jsx".into()],
21635 ..Default::default()
21636 },
21637 overrides: [(
21638 "element".into(),
21639 LanguageConfigOverride {
21640 completion_query_characters: Override::Set(['-'].into_iter().collect()),
21641 ..Default::default()
21642 },
21643 )]
21644 .into_iter()
21645 .collect(),
21646 ..Default::default()
21647 },
21648 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
21649 )
21650 .with_override_query("(jsx_self_closing_element) @element")
21651 .unwrap(),
21652 lsp::ServerCapabilities {
21653 completion_provider: Some(lsp::CompletionOptions {
21654 trigger_characters: Some(vec![":".to_string()]),
21655 ..Default::default()
21656 }),
21657 ..Default::default()
21658 },
21659 cx,
21660 )
21661 .await;
21662
21663 cx.lsp
21664 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21665 Ok(Some(lsp::CompletionResponse::Array(vec![
21666 lsp::CompletionItem {
21667 label: "bg-blue".into(),
21668 ..Default::default()
21669 },
21670 lsp::CompletionItem {
21671 label: "bg-red".into(),
21672 ..Default::default()
21673 },
21674 lsp::CompletionItem {
21675 label: "bg-yellow".into(),
21676 ..Default::default()
21677 },
21678 ])))
21679 });
21680
21681 cx.set_state(r#"<p class="bgˇ" />"#);
21682
21683 // Trigger completion when typing a dash, because the dash is an extra
21684 // word character in the 'element' scope, which contains the cursor.
21685 cx.simulate_keystroke("-");
21686 cx.executor().run_until_parked();
21687 cx.update_editor(|editor, _, _| {
21688 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
21689 {
21690 assert_eq!(
21691 completion_menu_entries(menu),
21692 &["bg-blue", "bg-red", "bg-yellow"]
21693 );
21694 } else {
21695 panic!("expected completion menu to be open");
21696 }
21697 });
21698
21699 cx.simulate_keystroke("l");
21700 cx.executor().run_until_parked();
21701 cx.update_editor(|editor, _, _| {
21702 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
21703 {
21704 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
21705 } else {
21706 panic!("expected completion menu to be open");
21707 }
21708 });
21709
21710 // When filtering completions, consider the character after the '-' to
21711 // be the start of a subword.
21712 cx.set_state(r#"<p class="yelˇ" />"#);
21713 cx.simulate_keystroke("l");
21714 cx.executor().run_until_parked();
21715 cx.update_editor(|editor, _, _| {
21716 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
21717 {
21718 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
21719 } else {
21720 panic!("expected completion menu to be open");
21721 }
21722 });
21723}
21724
21725fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
21726 let entries = menu.entries.borrow();
21727 entries.iter().map(|mat| mat.string.clone()).collect()
21728}
21729
21730#[gpui::test]
21731async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
21732 init_test(cx, |settings| {
21733 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21734 });
21735
21736 let fs = FakeFs::new(cx.executor());
21737 fs.insert_file(path!("/file.ts"), Default::default()).await;
21738
21739 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
21740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21741
21742 language_registry.add(Arc::new(Language::new(
21743 LanguageConfig {
21744 name: "TypeScript".into(),
21745 matcher: LanguageMatcher {
21746 path_suffixes: vec!["ts".to_string()],
21747 ..Default::default()
21748 },
21749 ..Default::default()
21750 },
21751 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21752 )));
21753 update_test_language_settings(cx, &|settings| {
21754 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21755 });
21756
21757 let test_plugin = "test_plugin";
21758 let _ = language_registry.register_fake_lsp(
21759 "TypeScript",
21760 FakeLspAdapter {
21761 prettier_plugins: vec![test_plugin],
21762 ..Default::default()
21763 },
21764 );
21765
21766 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21767 let buffer = project
21768 .update(cx, |project, cx| {
21769 project.open_local_buffer(path!("/file.ts"), cx)
21770 })
21771 .await
21772 .unwrap();
21773
21774 let buffer_text = "one\ntwo\nthree\n";
21775 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21776 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21777 editor.update_in(cx, |editor, window, cx| {
21778 editor.set_text(buffer_text, window, cx)
21779 });
21780
21781 editor
21782 .update_in(cx, |editor, window, cx| {
21783 editor.perform_format(
21784 project.clone(),
21785 FormatTrigger::Manual,
21786 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21787 window,
21788 cx,
21789 )
21790 })
21791 .unwrap()
21792 .await;
21793 assert_eq!(
21794 editor.update(cx, |editor, cx| editor.text(cx)),
21795 buffer_text.to_string() + prettier_format_suffix,
21796 "Test prettier formatting was not applied to the original buffer text",
21797 );
21798
21799 update_test_language_settings(cx, &|settings| {
21800 settings.defaults.formatter = Some(FormatterList::default())
21801 });
21802 let format = editor.update_in(cx, |editor, window, cx| {
21803 editor.perform_format(
21804 project.clone(),
21805 FormatTrigger::Manual,
21806 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21807 window,
21808 cx,
21809 )
21810 });
21811 format.await.unwrap();
21812 assert_eq!(
21813 editor.update(cx, |editor, cx| editor.text(cx)),
21814 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
21815 "Autoformatting (via test prettier) was not applied to the original buffer text",
21816 );
21817}
21818
21819#[gpui::test]
21820async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
21821 init_test(cx, |settings| {
21822 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21823 });
21824
21825 let fs = FakeFs::new(cx.executor());
21826 fs.insert_file(path!("/file.settings"), Default::default())
21827 .await;
21828
21829 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
21830 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21831
21832 let ts_lang = Arc::new(Language::new(
21833 LanguageConfig {
21834 name: "TypeScript".into(),
21835 matcher: LanguageMatcher {
21836 path_suffixes: vec!["ts".to_string()],
21837 ..LanguageMatcher::default()
21838 },
21839 prettier_parser_name: Some("typescript".to_string()),
21840 ..LanguageConfig::default()
21841 },
21842 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21843 ));
21844
21845 language_registry.add(ts_lang.clone());
21846
21847 update_test_language_settings(cx, &|settings| {
21848 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21849 });
21850
21851 let test_plugin = "test_plugin";
21852 let _ = language_registry.register_fake_lsp(
21853 "TypeScript",
21854 FakeLspAdapter {
21855 prettier_plugins: vec![test_plugin],
21856 ..Default::default()
21857 },
21858 );
21859
21860 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21861 let buffer = project
21862 .update(cx, |project, cx| {
21863 project.open_local_buffer(path!("/file.settings"), cx)
21864 })
21865 .await
21866 .unwrap();
21867
21868 project.update(cx, |project, cx| {
21869 project.set_language_for_buffer(&buffer, ts_lang, cx)
21870 });
21871
21872 let buffer_text = "one\ntwo\nthree\n";
21873 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21874 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21875 editor.update_in(cx, |editor, window, cx| {
21876 editor.set_text(buffer_text, window, cx)
21877 });
21878
21879 editor
21880 .update_in(cx, |editor, window, cx| {
21881 editor.perform_format(
21882 project.clone(),
21883 FormatTrigger::Manual,
21884 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21885 window,
21886 cx,
21887 )
21888 })
21889 .unwrap()
21890 .await;
21891 assert_eq!(
21892 editor.update(cx, |editor, cx| editor.text(cx)),
21893 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
21894 "Test prettier formatting was not applied to the original buffer text",
21895 );
21896
21897 update_test_language_settings(cx, &|settings| {
21898 settings.defaults.formatter = Some(FormatterList::default())
21899 });
21900 let format = editor.update_in(cx, |editor, window, cx| {
21901 editor.perform_format(
21902 project.clone(),
21903 FormatTrigger::Manual,
21904 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21905 window,
21906 cx,
21907 )
21908 });
21909 format.await.unwrap();
21910
21911 assert_eq!(
21912 editor.update(cx, |editor, cx| editor.text(cx)),
21913 buffer_text.to_string()
21914 + prettier_format_suffix
21915 + "\ntypescript\n"
21916 + prettier_format_suffix
21917 + "\ntypescript",
21918 "Autoformatting (via test prettier) was not applied to the original buffer text",
21919 );
21920}
21921
21922#[gpui::test]
21923async fn test_range_format_with_prettier(cx: &mut TestAppContext) {
21924 init_test(cx, |settings| {
21925 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21926 });
21927
21928 let fs = FakeFs::new(cx.executor());
21929 fs.insert_file(path!("/file.ts"), Default::default()).await;
21930
21931 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
21932 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21933
21934 language_registry.add(Arc::new(Language::new(
21935 LanguageConfig {
21936 name: "TypeScript".into(),
21937 matcher: LanguageMatcher {
21938 path_suffixes: vec!["ts".to_string()],
21939 ..Default::default()
21940 },
21941 ..Default::default()
21942 },
21943 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21944 )));
21945 update_test_language_settings(cx, &|settings| {
21946 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21947 });
21948
21949 let test_plugin = "test_plugin";
21950 let _ = language_registry.register_fake_lsp(
21951 "TypeScript",
21952 FakeLspAdapter {
21953 prettier_plugins: vec![test_plugin],
21954 ..Default::default()
21955 },
21956 );
21957
21958 let prettier_range_format_suffix = project::TEST_PRETTIER_RANGE_FORMAT_SUFFIX;
21959 let buffer = project
21960 .update(cx, |project, cx| {
21961 project.open_local_buffer(path!("/file.ts"), cx)
21962 })
21963 .await
21964 .unwrap();
21965
21966 let buffer_text = "one\ntwo\nthree\nfour\nfive\n";
21967 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21968 let (editor, cx) = cx.add_window_view(|window, cx| {
21969 build_editor_with_project(project.clone(), buffer, window, cx)
21970 });
21971 editor.update_in(cx, |editor, window, cx| {
21972 editor.set_text(buffer_text, window, cx)
21973 });
21974
21975 cx.executor().run_until_parked();
21976
21977 editor.update_in(cx, |editor, window, cx| {
21978 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
21979 s.select_ranges([Point::new(1, 0)..Point::new(3, 0)])
21980 });
21981 });
21982
21983 let format = editor
21984 .update_in(cx, |editor, window, cx| {
21985 editor.format_selections(&FormatSelections, window, cx)
21986 })
21987 .unwrap();
21988 format.await.unwrap();
21989
21990 assert_eq!(
21991 editor.update(cx, |editor, cx| editor.text(cx)),
21992 format!("one\ntwo{prettier_range_format_suffix}\nthree\nfour\nfive\n"),
21993 "Range formatting (via test prettier) was not applied to the buffer text",
21994 );
21995}
21996
21997#[gpui::test]
21998async fn test_range_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
21999 init_test(cx, |settings| {
22000 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
22001 });
22002
22003 let fs = FakeFs::new(cx.executor());
22004 fs.insert_file(path!("/file.settings"), Default::default())
22005 .await;
22006
22007 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
22008 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22009
22010 let ts_lang = Arc::new(Language::new(
22011 LanguageConfig {
22012 name: "TypeScript".into(),
22013 matcher: LanguageMatcher {
22014 path_suffixes: vec!["ts".to_string()],
22015 ..LanguageMatcher::default()
22016 },
22017 prettier_parser_name: Some("typescript".to_string()),
22018 ..LanguageConfig::default()
22019 },
22020 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22021 ));
22022
22023 language_registry.add(ts_lang.clone());
22024
22025 update_test_language_settings(cx, &|settings| {
22026 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
22027 });
22028
22029 let test_plugin = "test_plugin";
22030 let _ = language_registry.register_fake_lsp(
22031 "TypeScript",
22032 FakeLspAdapter {
22033 prettier_plugins: vec![test_plugin],
22034 ..Default::default()
22035 },
22036 );
22037
22038 let prettier_range_format_suffix = project::TEST_PRETTIER_RANGE_FORMAT_SUFFIX;
22039 let buffer = project
22040 .update(cx, |project, cx| {
22041 project.open_local_buffer(path!("/file.settings"), cx)
22042 })
22043 .await
22044 .unwrap();
22045
22046 project.update(cx, |project, cx| {
22047 project.set_language_for_buffer(&buffer, ts_lang, cx)
22048 });
22049
22050 let buffer_text = "one\ntwo\nthree\nfour\nfive\n";
22051 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
22052 let (editor, cx) = cx.add_window_view(|window, cx| {
22053 build_editor_with_project(project.clone(), buffer, window, cx)
22054 });
22055 editor.update_in(cx, |editor, window, cx| {
22056 editor.set_text(buffer_text, window, cx)
22057 });
22058
22059 cx.executor().run_until_parked();
22060
22061 editor.update_in(cx, |editor, window, cx| {
22062 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
22063 s.select_ranges([Point::new(1, 0)..Point::new(3, 0)])
22064 });
22065 });
22066
22067 let format = editor
22068 .update_in(cx, |editor, window, cx| {
22069 editor.format_selections(&FormatSelections, window, cx)
22070 })
22071 .unwrap();
22072 format.await.unwrap();
22073
22074 assert_eq!(
22075 editor.update(cx, |editor, cx| editor.text(cx)),
22076 format!("one\ntwo{prettier_range_format_suffix}\ntypescript\nthree\nfour\nfive\n"),
22077 "Range formatting (via test prettier) was not applied with explicit language",
22078 );
22079}
22080
22081#[gpui::test]
22082async fn test_addition_reverts(cx: &mut TestAppContext) {
22083 init_test(cx, |_| {});
22084 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22085 let base_text = indoc! {r#"
22086 struct Row;
22087 struct Row1;
22088 struct Row2;
22089
22090 struct Row4;
22091 struct Row5;
22092 struct Row6;
22093
22094 struct Row8;
22095 struct Row9;
22096 struct Row10;"#};
22097
22098 // When addition hunks are not adjacent to carets, no hunk revert is performed
22099 assert_hunk_revert(
22100 indoc! {r#"struct Row;
22101 struct Row1;
22102 struct Row1.1;
22103 struct Row1.2;
22104 struct Row2;ˇ
22105
22106 struct Row4;
22107 struct Row5;
22108 struct Row6;
22109
22110 struct Row8;
22111 ˇstruct Row9;
22112 struct Row9.1;
22113 struct Row9.2;
22114 struct Row9.3;
22115 struct Row10;"#},
22116 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
22117 indoc! {r#"struct Row;
22118 struct Row1;
22119 struct Row1.1;
22120 struct Row1.2;
22121 struct Row2;ˇ
22122
22123 struct Row4;
22124 struct Row5;
22125 struct Row6;
22126
22127 struct Row8;
22128 ˇstruct Row9;
22129 struct Row9.1;
22130 struct Row9.2;
22131 struct Row9.3;
22132 struct Row10;"#},
22133 base_text,
22134 &mut cx,
22135 );
22136 // Same for selections
22137 assert_hunk_revert(
22138 indoc! {r#"struct Row;
22139 struct Row1;
22140 struct Row2;
22141 struct Row2.1;
22142 struct Row2.2;
22143 «ˇ
22144 struct Row4;
22145 struct» Row5;
22146 «struct Row6;
22147 ˇ»
22148 struct Row9.1;
22149 struct Row9.2;
22150 struct Row9.3;
22151 struct Row8;
22152 struct Row9;
22153 struct Row10;"#},
22154 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
22155 indoc! {r#"struct Row;
22156 struct Row1;
22157 struct Row2;
22158 struct Row2.1;
22159 struct Row2.2;
22160 «ˇ
22161 struct Row4;
22162 struct» Row5;
22163 «struct Row6;
22164 ˇ»
22165 struct Row9.1;
22166 struct Row9.2;
22167 struct Row9.3;
22168 struct Row8;
22169 struct Row9;
22170 struct Row10;"#},
22171 base_text,
22172 &mut cx,
22173 );
22174
22175 // When carets and selections intersect the addition hunks, those are reverted.
22176 // Adjacent carets got merged.
22177 assert_hunk_revert(
22178 indoc! {r#"struct Row;
22179 ˇ// something on the top
22180 struct Row1;
22181 struct Row2;
22182 struct Roˇw3.1;
22183 struct Row2.2;
22184 struct Row2.3;ˇ
22185
22186 struct Row4;
22187 struct ˇRow5.1;
22188 struct Row5.2;
22189 struct «Rowˇ»5.3;
22190 struct Row5;
22191 struct Row6;
22192 ˇ
22193 struct Row9.1;
22194 struct «Rowˇ»9.2;
22195 struct «ˇRow»9.3;
22196 struct Row8;
22197 struct Row9;
22198 «ˇ// something on bottom»
22199 struct Row10;"#},
22200 vec![
22201 DiffHunkStatusKind::Added,
22202 DiffHunkStatusKind::Added,
22203 DiffHunkStatusKind::Added,
22204 DiffHunkStatusKind::Added,
22205 DiffHunkStatusKind::Added,
22206 ],
22207 indoc! {r#"struct Row;
22208 ˇstruct Row1;
22209 struct Row2;
22210 ˇ
22211 struct Row4;
22212 ˇstruct Row5;
22213 struct Row6;
22214 ˇ
22215 ˇstruct Row8;
22216 struct Row9;
22217 ˇstruct Row10;"#},
22218 base_text,
22219 &mut cx,
22220 );
22221}
22222
22223#[gpui::test]
22224async fn test_modification_reverts(cx: &mut TestAppContext) {
22225 init_test(cx, |_| {});
22226 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22227 let base_text = indoc! {r#"
22228 struct Row;
22229 struct Row1;
22230 struct Row2;
22231
22232 struct Row4;
22233 struct Row5;
22234 struct Row6;
22235
22236 struct Row8;
22237 struct Row9;
22238 struct Row10;"#};
22239
22240 // Modification hunks behave the same as the addition ones.
22241 assert_hunk_revert(
22242 indoc! {r#"struct Row;
22243 struct Row1;
22244 struct Row33;
22245 ˇ
22246 struct Row4;
22247 struct Row5;
22248 struct Row6;
22249 ˇ
22250 struct Row99;
22251 struct Row9;
22252 struct Row10;"#},
22253 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
22254 indoc! {r#"struct Row;
22255 struct Row1;
22256 struct Row33;
22257 ˇ
22258 struct Row4;
22259 struct Row5;
22260 struct Row6;
22261 ˇ
22262 struct Row99;
22263 struct Row9;
22264 struct Row10;"#},
22265 base_text,
22266 &mut cx,
22267 );
22268 assert_hunk_revert(
22269 indoc! {r#"struct Row;
22270 struct Row1;
22271 struct Row33;
22272 «ˇ
22273 struct Row4;
22274 struct» Row5;
22275 «struct Row6;
22276 ˇ»
22277 struct Row99;
22278 struct Row9;
22279 struct Row10;"#},
22280 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
22281 indoc! {r#"struct Row;
22282 struct Row1;
22283 struct Row33;
22284 «ˇ
22285 struct Row4;
22286 struct» Row5;
22287 «struct Row6;
22288 ˇ»
22289 struct Row99;
22290 struct Row9;
22291 struct Row10;"#},
22292 base_text,
22293 &mut cx,
22294 );
22295
22296 assert_hunk_revert(
22297 indoc! {r#"ˇstruct Row1.1;
22298 struct Row1;
22299 «ˇstr»uct Row22;
22300
22301 struct ˇRow44;
22302 struct Row5;
22303 struct «Rˇ»ow66;ˇ
22304
22305 «struˇ»ct Row88;
22306 struct Row9;
22307 struct Row1011;ˇ"#},
22308 vec![
22309 DiffHunkStatusKind::Modified,
22310 DiffHunkStatusKind::Modified,
22311 DiffHunkStatusKind::Modified,
22312 DiffHunkStatusKind::Modified,
22313 DiffHunkStatusKind::Modified,
22314 DiffHunkStatusKind::Modified,
22315 ],
22316 indoc! {r#"struct Row;
22317 ˇstruct Row1;
22318 struct Row2;
22319 ˇ
22320 struct Row4;
22321 ˇstruct Row5;
22322 struct Row6;
22323 ˇ
22324 struct Row8;
22325 ˇstruct Row9;
22326 struct Row10;ˇ"#},
22327 base_text,
22328 &mut cx,
22329 );
22330}
22331
22332#[gpui::test]
22333async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
22334 init_test(cx, |_| {});
22335 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22336 let base_text = indoc! {r#"
22337 one
22338
22339 two
22340 three
22341 "#};
22342
22343 cx.set_head_text(base_text);
22344 cx.set_state("\nˇ\n");
22345 cx.executor().run_until_parked();
22346 cx.update_editor(|editor, _window, cx| {
22347 editor.expand_selected_diff_hunks(cx);
22348 });
22349 cx.executor().run_until_parked();
22350 cx.update_editor(|editor, window, cx| {
22351 editor.backspace(&Default::default(), window, cx);
22352 });
22353 cx.run_until_parked();
22354 cx.assert_state_with_diff(
22355 indoc! {r#"
22356
22357 - two
22358 - threeˇ
22359 +
22360 "#}
22361 .to_string(),
22362 );
22363}
22364
22365#[gpui::test]
22366async fn test_deletion_reverts(cx: &mut TestAppContext) {
22367 init_test(cx, |_| {});
22368 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22369 let base_text = indoc! {r#"struct Row;
22370struct Row1;
22371struct Row2;
22372
22373struct Row4;
22374struct Row5;
22375struct Row6;
22376
22377struct Row8;
22378struct Row9;
22379struct Row10;"#};
22380
22381 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
22382 assert_hunk_revert(
22383 indoc! {r#"struct Row;
22384 struct Row2;
22385
22386 ˇstruct Row4;
22387 struct Row5;
22388 struct Row6;
22389 ˇ
22390 struct Row8;
22391 struct Row10;"#},
22392 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22393 indoc! {r#"struct Row;
22394 struct Row2;
22395
22396 ˇstruct Row4;
22397 struct Row5;
22398 struct Row6;
22399 ˇ
22400 struct Row8;
22401 struct Row10;"#},
22402 base_text,
22403 &mut cx,
22404 );
22405 assert_hunk_revert(
22406 indoc! {r#"struct Row;
22407 struct Row2;
22408
22409 «ˇstruct Row4;
22410 struct» Row5;
22411 «struct Row6;
22412 ˇ»
22413 struct Row8;
22414 struct Row10;"#},
22415 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22416 indoc! {r#"struct Row;
22417 struct Row2;
22418
22419 «ˇstruct Row4;
22420 struct» Row5;
22421 «struct Row6;
22422 ˇ»
22423 struct Row8;
22424 struct Row10;"#},
22425 base_text,
22426 &mut cx,
22427 );
22428
22429 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
22430 assert_hunk_revert(
22431 indoc! {r#"struct Row;
22432 ˇstruct Row2;
22433
22434 struct Row4;
22435 struct Row5;
22436 struct Row6;
22437
22438 struct Row8;ˇ
22439 struct Row10;"#},
22440 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22441 indoc! {r#"struct Row;
22442 struct Row1;
22443 ˇstruct Row2;
22444
22445 struct Row4;
22446 struct Row5;
22447 struct Row6;
22448
22449 struct Row8;ˇ
22450 struct Row9;
22451 struct Row10;"#},
22452 base_text,
22453 &mut cx,
22454 );
22455 assert_hunk_revert(
22456 indoc! {r#"struct Row;
22457 struct Row2«ˇ;
22458 struct Row4;
22459 struct» Row5;
22460 «struct Row6;
22461
22462 struct Row8;ˇ»
22463 struct Row10;"#},
22464 vec![
22465 DiffHunkStatusKind::Deleted,
22466 DiffHunkStatusKind::Deleted,
22467 DiffHunkStatusKind::Deleted,
22468 ],
22469 indoc! {r#"struct Row;
22470 struct Row1;
22471 struct Row2«ˇ;
22472
22473 struct Row4;
22474 struct» Row5;
22475 «struct Row6;
22476
22477 struct Row8;ˇ»
22478 struct Row9;
22479 struct Row10;"#},
22480 base_text,
22481 &mut cx,
22482 );
22483}
22484
22485#[gpui::test]
22486async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
22487 init_test(cx, |_| {});
22488
22489 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
22490 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
22491 let base_text_3 =
22492 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
22493
22494 let text_1 = edit_first_char_of_every_line(base_text_1);
22495 let text_2 = edit_first_char_of_every_line(base_text_2);
22496 let text_3 = edit_first_char_of_every_line(base_text_3);
22497
22498 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
22499 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
22500 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
22501
22502 let multibuffer = cx.new(|cx| {
22503 let mut multibuffer = MultiBuffer::new(ReadWrite);
22504 multibuffer.set_excerpts_for_path(
22505 PathKey::sorted(0),
22506 buffer_1.clone(),
22507 [
22508 Point::new(0, 0)..Point::new(2, 0),
22509 Point::new(5, 0)..Point::new(6, 0),
22510 Point::new(9, 0)..Point::new(9, 4),
22511 ],
22512 0,
22513 cx,
22514 );
22515 multibuffer.set_excerpts_for_path(
22516 PathKey::sorted(1),
22517 buffer_2.clone(),
22518 [
22519 Point::new(0, 0)..Point::new(2, 0),
22520 Point::new(5, 0)..Point::new(6, 0),
22521 Point::new(9, 0)..Point::new(9, 4),
22522 ],
22523 0,
22524 cx,
22525 );
22526 multibuffer.set_excerpts_for_path(
22527 PathKey::sorted(2),
22528 buffer_3.clone(),
22529 [
22530 Point::new(0, 0)..Point::new(2, 0),
22531 Point::new(5, 0)..Point::new(6, 0),
22532 Point::new(9, 0)..Point::new(9, 4),
22533 ],
22534 0,
22535 cx,
22536 );
22537 multibuffer
22538 });
22539
22540 let fs = FakeFs::new(cx.executor());
22541 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22542 let (editor, cx) = cx
22543 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
22544 editor.update_in(cx, |editor, _window, cx| {
22545 for (buffer, diff_base) in [
22546 (buffer_1.clone(), base_text_1),
22547 (buffer_2.clone(), base_text_2),
22548 (buffer_3.clone(), base_text_3),
22549 ] {
22550 let diff = cx.new(|cx| {
22551 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
22552 });
22553 editor
22554 .buffer
22555 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
22556 }
22557 });
22558 cx.executor().run_until_parked();
22559
22560 editor.update_in(cx, |editor, window, cx| {
22561 assert_eq!(editor.display_text(cx), "\n\nXaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\n\n\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
22562 editor.select_all(&SelectAll, window, cx);
22563 editor.git_restore(&Default::default(), window, cx);
22564 });
22565 cx.executor().run_until_parked();
22566
22567 // When all ranges are selected, all buffer hunks are reverted.
22568 editor.update(cx, |editor, cx| {
22569 assert_eq!(editor.display_text(cx), "\n\naaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\n\n\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n\n\n");
22570 });
22571 buffer_1.update(cx, |buffer, _| {
22572 assert_eq!(buffer.text(), base_text_1);
22573 });
22574 buffer_2.update(cx, |buffer, _| {
22575 assert_eq!(buffer.text(), base_text_2);
22576 });
22577 buffer_3.update(cx, |buffer, _| {
22578 assert_eq!(buffer.text(), base_text_3);
22579 });
22580
22581 editor.update_in(cx, |editor, window, cx| {
22582 editor.undo(&Default::default(), window, cx);
22583 });
22584
22585 editor.update_in(cx, |editor, window, cx| {
22586 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22587 s.select_ranges(Some(Point::new(0, 0)..Point::new(5, 0)));
22588 });
22589 editor.git_restore(&Default::default(), window, cx);
22590 });
22591
22592 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
22593 // but not affect buffer_2 and its related excerpts.
22594 editor.update(cx, |editor, cx| {
22595 assert_eq!(
22596 editor.display_text(cx),
22597 "\n\naaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\n\n\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\n\n\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
22598 );
22599 });
22600 buffer_1.update(cx, |buffer, _| {
22601 assert_eq!(buffer.text(), base_text_1);
22602 });
22603 buffer_2.update(cx, |buffer, _| {
22604 assert_eq!(
22605 buffer.text(),
22606 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
22607 );
22608 });
22609 buffer_3.update(cx, |buffer, _| {
22610 assert_eq!(
22611 buffer.text(),
22612 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
22613 );
22614 });
22615
22616 fn edit_first_char_of_every_line(text: &str) -> String {
22617 text.split('\n')
22618 .map(|line| format!("X{}", &line[1..]))
22619 .collect::<Vec<_>>()
22620 .join("\n")
22621 }
22622}
22623
22624#[gpui::test]
22625async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
22626 init_test(cx, |_| {});
22627
22628 let cols = 4;
22629 let rows = 10;
22630 let sample_text_1 = sample_text(rows, cols, 'a');
22631 assert_eq!(
22632 sample_text_1,
22633 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
22634 );
22635 let sample_text_2 = sample_text(rows, cols, 'l');
22636 assert_eq!(
22637 sample_text_2,
22638 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
22639 );
22640 let sample_text_3 = sample_text(rows, cols, 'v');
22641 assert_eq!(
22642 sample_text_3,
22643 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
22644 );
22645
22646 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
22647 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
22648 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
22649
22650 let multi_buffer = cx.new(|cx| {
22651 let mut multibuffer = MultiBuffer::new(ReadWrite);
22652 multibuffer.set_excerpts_for_path(
22653 PathKey::sorted(0),
22654 buffer_1.clone(),
22655 [
22656 Point::new(0, 0)..Point::new(2, 0),
22657 Point::new(5, 0)..Point::new(6, 0),
22658 Point::new(9, 0)..Point::new(9, 4),
22659 ],
22660 0,
22661 cx,
22662 );
22663 multibuffer.set_excerpts_for_path(
22664 PathKey::sorted(1),
22665 buffer_2.clone(),
22666 [
22667 Point::new(0, 0)..Point::new(2, 0),
22668 Point::new(5, 0)..Point::new(6, 0),
22669 Point::new(9, 0)..Point::new(9, 4),
22670 ],
22671 0,
22672 cx,
22673 );
22674 multibuffer.set_excerpts_for_path(
22675 PathKey::sorted(2),
22676 buffer_3.clone(),
22677 [
22678 Point::new(0, 0)..Point::new(2, 0),
22679 Point::new(5, 0)..Point::new(6, 0),
22680 Point::new(9, 0)..Point::new(9, 4),
22681 ],
22682 0,
22683 cx,
22684 );
22685 multibuffer
22686 });
22687
22688 let fs = FakeFs::new(cx.executor());
22689 fs.insert_tree(
22690 "/a",
22691 json!({
22692 "main.rs": sample_text_1,
22693 "other.rs": sample_text_2,
22694 "lib.rs": sample_text_3,
22695 }),
22696 )
22697 .await;
22698 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22699 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
22700 let workspace = window
22701 .read_with(cx, |mw, _| mw.workspace().clone())
22702 .unwrap();
22703 let cx = &mut VisualTestContext::from_window(*window, cx);
22704 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22705 Editor::new(
22706 EditorMode::full(),
22707 multi_buffer,
22708 Some(project.clone()),
22709 window,
22710 cx,
22711 )
22712 });
22713 let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| {
22714 assert!(
22715 workspace.active_item(cx).is_none(),
22716 "active item should be None before the first item is added"
22717 );
22718 workspace.add_item_to_active_pane(
22719 Box::new(multi_buffer_editor.clone()),
22720 None,
22721 true,
22722 window,
22723 cx,
22724 );
22725 let active_item = workspace
22726 .active_item(cx)
22727 .expect("should have an active item after adding the multi buffer");
22728 assert_eq!(
22729 active_item.buffer_kind(cx),
22730 ItemBufferKind::Multibuffer,
22731 "A multi buffer was expected to active after adding"
22732 );
22733 active_item.item_id()
22734 });
22735
22736 cx.executor().run_until_parked();
22737
22738 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22739 editor.change_selections(
22740 SelectionEffects::scroll(Autoscroll::Next),
22741 window,
22742 cx,
22743 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
22744 );
22745 editor.open_excerpts(&OpenExcerpts, window, cx);
22746 });
22747 cx.executor().run_until_parked();
22748 let first_item_id = workspace.update_in(cx, |workspace, window, cx| {
22749 let active_item = workspace
22750 .active_item(cx)
22751 .expect("should have an active item after navigating into the 1st buffer");
22752 let first_item_id = active_item.item_id();
22753 assert_ne!(
22754 first_item_id, multibuffer_item_id,
22755 "Should navigate into the 1st buffer and activate it"
22756 );
22757 assert_eq!(
22758 active_item.buffer_kind(cx),
22759 ItemBufferKind::Singleton,
22760 "New active item should be a singleton buffer"
22761 );
22762 assert_eq!(
22763 active_item
22764 .act_as::<Editor>(cx)
22765 .expect("should have navigated into an editor for the 1st buffer")
22766 .read(cx)
22767 .text(cx),
22768 sample_text_1
22769 );
22770
22771 workspace
22772 .go_back(workspace.active_pane().downgrade(), window, cx)
22773 .detach_and_log_err(cx);
22774
22775 first_item_id
22776 });
22777
22778 cx.executor().run_until_parked();
22779 workspace.update_in(cx, |workspace, _, cx| {
22780 let active_item = workspace
22781 .active_item(cx)
22782 .expect("should have an active item after navigating back");
22783 assert_eq!(
22784 active_item.item_id(),
22785 multibuffer_item_id,
22786 "Should navigate back to the multi buffer"
22787 );
22788 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22789 });
22790
22791 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22792 editor.change_selections(
22793 SelectionEffects::scroll(Autoscroll::Next),
22794 window,
22795 cx,
22796 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
22797 );
22798 editor.open_excerpts(&OpenExcerpts, window, cx);
22799 });
22800 cx.executor().run_until_parked();
22801 let second_item_id = workspace.update_in(cx, |workspace, window, cx| {
22802 let active_item = workspace
22803 .active_item(cx)
22804 .expect("should have an active item after navigating into the 2nd buffer");
22805 let second_item_id = active_item.item_id();
22806 assert_ne!(
22807 second_item_id, multibuffer_item_id,
22808 "Should navigate away from the multibuffer"
22809 );
22810 assert_ne!(
22811 second_item_id, first_item_id,
22812 "Should navigate into the 2nd buffer and activate it"
22813 );
22814 assert_eq!(
22815 active_item.buffer_kind(cx),
22816 ItemBufferKind::Singleton,
22817 "New active item should be a singleton buffer"
22818 );
22819 assert_eq!(
22820 active_item
22821 .act_as::<Editor>(cx)
22822 .expect("should have navigated into an editor")
22823 .read(cx)
22824 .text(cx),
22825 sample_text_2
22826 );
22827
22828 workspace
22829 .go_back(workspace.active_pane().downgrade(), window, cx)
22830 .detach_and_log_err(cx);
22831
22832 second_item_id
22833 });
22834
22835 cx.executor().run_until_parked();
22836 workspace.update_in(cx, |workspace, _, cx| {
22837 let active_item = workspace
22838 .active_item(cx)
22839 .expect("should have an active item after navigating back from the 2nd buffer");
22840 assert_eq!(
22841 active_item.item_id(),
22842 multibuffer_item_id,
22843 "Should navigate back from the 2nd buffer to the multi buffer"
22844 );
22845 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22846 });
22847
22848 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22849 editor.change_selections(
22850 SelectionEffects::scroll(Autoscroll::Next),
22851 window,
22852 cx,
22853 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
22854 );
22855 editor.open_excerpts(&OpenExcerpts, window, cx);
22856 });
22857 cx.executor().run_until_parked();
22858 workspace.update_in(cx, |workspace, window, cx| {
22859 let active_item = workspace
22860 .active_item(cx)
22861 .expect("should have an active item after navigating into the 3rd buffer");
22862 let third_item_id = active_item.item_id();
22863 assert_ne!(
22864 third_item_id, multibuffer_item_id,
22865 "Should navigate into the 3rd buffer and activate it"
22866 );
22867 assert_ne!(third_item_id, first_item_id);
22868 assert_ne!(third_item_id, second_item_id);
22869 assert_eq!(
22870 active_item.buffer_kind(cx),
22871 ItemBufferKind::Singleton,
22872 "New active item should be a singleton buffer"
22873 );
22874 assert_eq!(
22875 active_item
22876 .act_as::<Editor>(cx)
22877 .expect("should have navigated into an editor")
22878 .read(cx)
22879 .text(cx),
22880 sample_text_3
22881 );
22882
22883 workspace
22884 .go_back(workspace.active_pane().downgrade(), window, cx)
22885 .detach_and_log_err(cx);
22886 });
22887
22888 cx.executor().run_until_parked();
22889 workspace.update_in(cx, |workspace, _, cx| {
22890 let active_item = workspace
22891 .active_item(cx)
22892 .expect("should have an active item after navigating back from the 3rd buffer");
22893 assert_eq!(
22894 active_item.item_id(),
22895 multibuffer_item_id,
22896 "Should navigate back from the 3rd buffer to the multi buffer"
22897 );
22898 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22899 });
22900}
22901
22902#[gpui::test]
22903async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22904 init_test(cx, |_| {});
22905
22906 let mut cx = EditorTestContext::new(cx).await;
22907
22908 let diff_base = r#"
22909 use some::mod;
22910
22911 const A: u32 = 42;
22912
22913 fn main() {
22914 println!("hello");
22915
22916 println!("world");
22917 }
22918 "#
22919 .unindent();
22920
22921 cx.set_state(
22922 &r#"
22923 use some::modified;
22924
22925 ˇ
22926 fn main() {
22927 println!("hello there");
22928
22929 println!("around the");
22930 println!("world");
22931 }
22932 "#
22933 .unindent(),
22934 );
22935
22936 cx.set_head_text(&diff_base);
22937 executor.run_until_parked();
22938
22939 cx.update_editor(|editor, window, cx| {
22940 editor.go_to_next_hunk(&GoToHunk, window, cx);
22941 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22942 });
22943 executor.run_until_parked();
22944 cx.assert_state_with_diff(
22945 r#"
22946 use some::modified;
22947
22948
22949 fn main() {
22950 - println!("hello");
22951 + ˇ println!("hello there");
22952
22953 println!("around the");
22954 println!("world");
22955 }
22956 "#
22957 .unindent(),
22958 );
22959
22960 cx.update_editor(|editor, window, cx| {
22961 for _ in 0..2 {
22962 editor.go_to_next_hunk(&GoToHunk, window, cx);
22963 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22964 }
22965 });
22966 executor.run_until_parked();
22967 cx.assert_state_with_diff(
22968 r#"
22969 - use some::mod;
22970 + ˇuse some::modified;
22971
22972
22973 fn main() {
22974 - println!("hello");
22975 + println!("hello there");
22976
22977 + println!("around the");
22978 println!("world");
22979 }
22980 "#
22981 .unindent(),
22982 );
22983
22984 cx.update_editor(|editor, window, cx| {
22985 editor.go_to_next_hunk(&GoToHunk, window, cx);
22986 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22987 });
22988 executor.run_until_parked();
22989 cx.assert_state_with_diff(
22990 r#"
22991 - use some::mod;
22992 + use some::modified;
22993
22994 - const A: u32 = 42;
22995 ˇ
22996 fn main() {
22997 - println!("hello");
22998 + println!("hello there");
22999
23000 + println!("around the");
23001 println!("world");
23002 }
23003 "#
23004 .unindent(),
23005 );
23006
23007 cx.update_editor(|editor, window, cx| {
23008 editor.cancel(&Cancel, window, cx);
23009 });
23010
23011 cx.assert_state_with_diff(
23012 r#"
23013 use some::modified;
23014
23015 ˇ
23016 fn main() {
23017 println!("hello there");
23018
23019 println!("around the");
23020 println!("world");
23021 }
23022 "#
23023 .unindent(),
23024 );
23025}
23026
23027#[gpui::test]
23028async fn test_diff_base_change_with_expanded_diff_hunks(
23029 executor: BackgroundExecutor,
23030 cx: &mut TestAppContext,
23031) {
23032 init_test(cx, |_| {});
23033
23034 let mut cx = EditorTestContext::new(cx).await;
23035
23036 let diff_base = r#"
23037 use some::mod1;
23038 use some::mod2;
23039
23040 const A: u32 = 42;
23041 const B: u32 = 42;
23042 const C: u32 = 42;
23043
23044 fn main() {
23045 println!("hello");
23046
23047 println!("world");
23048 }
23049 "#
23050 .unindent();
23051
23052 cx.set_state(
23053 &r#"
23054 use some::mod2;
23055
23056 const A: u32 = 42;
23057 const C: u32 = 42;
23058
23059 fn main(ˇ) {
23060 //println!("hello");
23061
23062 println!("world");
23063 //
23064 //
23065 }
23066 "#
23067 .unindent(),
23068 );
23069
23070 cx.set_head_text(&diff_base);
23071 executor.run_until_parked();
23072
23073 cx.update_editor(|editor, window, cx| {
23074 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23075 });
23076 executor.run_until_parked();
23077 cx.assert_state_with_diff(
23078 r#"
23079 - use some::mod1;
23080 use some::mod2;
23081
23082 const A: u32 = 42;
23083 - const B: u32 = 42;
23084 const C: u32 = 42;
23085
23086 fn main(ˇ) {
23087 - println!("hello");
23088 + //println!("hello");
23089
23090 println!("world");
23091 + //
23092 + //
23093 }
23094 "#
23095 .unindent(),
23096 );
23097
23098 cx.set_head_text("new diff base!");
23099 executor.run_until_parked();
23100 cx.assert_state_with_diff(
23101 r#"
23102 - new diff base!
23103 + use some::mod2;
23104 +
23105 + const A: u32 = 42;
23106 + const C: u32 = 42;
23107 +
23108 + fn main(ˇ) {
23109 + //println!("hello");
23110 +
23111 + println!("world");
23112 + //
23113 + //
23114 + }
23115 "#
23116 .unindent(),
23117 );
23118}
23119
23120#[gpui::test]
23121async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
23122 init_test(cx, |_| {});
23123
23124 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
23125 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
23126 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
23127 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
23128 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
23129 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
23130
23131 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
23132 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
23133 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
23134
23135 let multi_buffer = cx.new(|cx| {
23136 let mut multibuffer = MultiBuffer::new(ReadWrite);
23137 multibuffer.set_excerpts_for_path(
23138 PathKey::sorted(0),
23139 buffer_1.clone(),
23140 [
23141 Point::new(0, 0)..Point::new(2, 3),
23142 Point::new(5, 0)..Point::new(6, 3),
23143 Point::new(9, 0)..Point::new(10, 3),
23144 ],
23145 0,
23146 cx,
23147 );
23148 multibuffer.set_excerpts_for_path(
23149 PathKey::sorted(1),
23150 buffer_2.clone(),
23151 [
23152 Point::new(0, 0)..Point::new(2, 3),
23153 Point::new(5, 0)..Point::new(6, 3),
23154 Point::new(9, 0)..Point::new(10, 3),
23155 ],
23156 0,
23157 cx,
23158 );
23159 multibuffer.set_excerpts_for_path(
23160 PathKey::sorted(2),
23161 buffer_3.clone(),
23162 [
23163 Point::new(0, 0)..Point::new(2, 3),
23164 Point::new(5, 0)..Point::new(6, 3),
23165 Point::new(9, 0)..Point::new(10, 3),
23166 ],
23167 0,
23168 cx,
23169 );
23170 assert_eq!(multibuffer.read(cx).excerpts().count(), 9);
23171 multibuffer
23172 });
23173
23174 let editor =
23175 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
23176 editor
23177 .update(cx, |editor, _window, cx| {
23178 for (buffer, diff_base) in [
23179 (buffer_1.clone(), file_1_old),
23180 (buffer_2.clone(), file_2_old),
23181 (buffer_3.clone(), file_3_old),
23182 ] {
23183 let diff = cx.new(|cx| {
23184 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
23185 });
23186 editor
23187 .buffer
23188 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
23189 }
23190 })
23191 .unwrap();
23192
23193 let mut cx = EditorTestContext::for_editor(editor, cx).await;
23194 cx.run_until_parked();
23195
23196 cx.assert_editor_state(
23197 &"
23198 ˇaaa
23199 ccc
23200 ddd
23201 ggg
23202 hhh
23203
23204 lll
23205 mmm
23206 NNN
23207 qqq
23208 rrr
23209 uuu
23210 111
23211 222
23212 333
23213 666
23214 777
23215 000
23216 !!!"
23217 .unindent(),
23218 );
23219
23220 cx.update_editor(|editor, window, cx| {
23221 editor.select_all(&SelectAll, window, cx);
23222 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23223 });
23224 cx.executor().run_until_parked();
23225
23226 cx.assert_state_with_diff(
23227 "
23228 «aaa
23229 - bbb
23230 ccc
23231 ddd
23232 ggg
23233 hhh
23234
23235 lll
23236 mmm
23237 - nnn
23238 + NNN
23239 qqq
23240 rrr
23241 uuu
23242 111
23243 222
23244 333
23245 + 666
23246 777
23247 000
23248 !!!ˇ»"
23249 .unindent(),
23250 );
23251}
23252
23253#[gpui::test]
23254async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
23255 init_test(cx, |_| {});
23256
23257 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
23258 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
23259
23260 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
23261 let multi_buffer = cx.new(|cx| {
23262 let mut multibuffer = MultiBuffer::new(ReadWrite);
23263 multibuffer.set_excerpts_for_path(
23264 PathKey::sorted(0),
23265 buffer.clone(),
23266 [
23267 Point::new(0, 0)..Point::new(1, 3),
23268 Point::new(4, 0)..Point::new(6, 3),
23269 Point::new(9, 0)..Point::new(9, 3),
23270 ],
23271 0,
23272 cx,
23273 );
23274 assert_eq!(multibuffer.read(cx).excerpts().count(), 3);
23275 multibuffer
23276 });
23277
23278 let editor =
23279 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
23280 editor
23281 .update(cx, |editor, _window, cx| {
23282 let diff = cx.new(|cx| {
23283 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
23284 });
23285 editor
23286 .buffer
23287 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
23288 })
23289 .unwrap();
23290
23291 let mut cx = EditorTestContext::for_editor(editor, cx).await;
23292 cx.run_until_parked();
23293
23294 cx.update_editor(|editor, window, cx| {
23295 editor.expand_all_diff_hunks(&Default::default(), window, cx)
23296 });
23297 cx.executor().run_until_parked();
23298
23299 // When the start of a hunk coincides with the start of its excerpt,
23300 // the hunk is expanded. When the start of a hunk is earlier than
23301 // the start of its excerpt, the hunk is not expanded.
23302 cx.assert_state_with_diff(
23303 "
23304 ˇaaa
23305 - bbb
23306 + BBB
23307 - ddd
23308 - eee
23309 + DDD
23310 + EEE
23311 fff
23312 iii"
23313 .unindent(),
23314 );
23315}
23316
23317#[gpui::test]
23318async fn test_edits_around_expanded_insertion_hunks(
23319 executor: BackgroundExecutor,
23320 cx: &mut TestAppContext,
23321) {
23322 init_test(cx, |_| {});
23323
23324 let mut cx = EditorTestContext::new(cx).await;
23325
23326 let diff_base = r#"
23327 use some::mod1;
23328 use some::mod2;
23329
23330 const A: u32 = 42;
23331
23332 fn main() {
23333 println!("hello");
23334
23335 println!("world");
23336 }
23337 "#
23338 .unindent();
23339 executor.run_until_parked();
23340 cx.set_state(
23341 &r#"
23342 use some::mod1;
23343 use some::mod2;
23344
23345 const A: u32 = 42;
23346 const B: u32 = 42;
23347 const C: u32 = 42;
23348 ˇ
23349
23350 fn main() {
23351 println!("hello");
23352
23353 println!("world");
23354 }
23355 "#
23356 .unindent(),
23357 );
23358
23359 cx.set_head_text(&diff_base);
23360 executor.run_until_parked();
23361
23362 cx.update_editor(|editor, window, cx| {
23363 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23364 });
23365 executor.run_until_parked();
23366
23367 cx.assert_state_with_diff(
23368 r#"
23369 use some::mod1;
23370 use some::mod2;
23371
23372 const A: u32 = 42;
23373 + const B: u32 = 42;
23374 + const C: u32 = 42;
23375 + ˇ
23376
23377 fn main() {
23378 println!("hello");
23379
23380 println!("world");
23381 }
23382 "#
23383 .unindent(),
23384 );
23385
23386 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
23387 executor.run_until_parked();
23388
23389 cx.assert_state_with_diff(
23390 r#"
23391 use some::mod1;
23392 use some::mod2;
23393
23394 const A: u32 = 42;
23395 + const B: u32 = 42;
23396 + const C: u32 = 42;
23397 + const D: u32 = 42;
23398 + ˇ
23399
23400 fn main() {
23401 println!("hello");
23402
23403 println!("world");
23404 }
23405 "#
23406 .unindent(),
23407 );
23408
23409 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
23410 executor.run_until_parked();
23411
23412 cx.assert_state_with_diff(
23413 r#"
23414 use some::mod1;
23415 use some::mod2;
23416
23417 const A: u32 = 42;
23418 + const B: u32 = 42;
23419 + const C: u32 = 42;
23420 + const D: u32 = 42;
23421 + const E: u32 = 42;
23422 + ˇ
23423
23424 fn main() {
23425 println!("hello");
23426
23427 println!("world");
23428 }
23429 "#
23430 .unindent(),
23431 );
23432
23433 cx.update_editor(|editor, window, cx| {
23434 editor.delete_line(&DeleteLine, window, cx);
23435 });
23436 executor.run_until_parked();
23437
23438 cx.assert_state_with_diff(
23439 r#"
23440 use some::mod1;
23441 use some::mod2;
23442
23443 const A: u32 = 42;
23444 + const B: u32 = 42;
23445 + const C: u32 = 42;
23446 + const D: u32 = 42;
23447 + const E: u32 = 42;
23448 ˇ
23449 fn main() {
23450 println!("hello");
23451
23452 println!("world");
23453 }
23454 "#
23455 .unindent(),
23456 );
23457
23458 cx.update_editor(|editor, window, cx| {
23459 editor.move_up(&MoveUp, window, cx);
23460 editor.delete_line(&DeleteLine, window, cx);
23461 editor.move_up(&MoveUp, window, cx);
23462 editor.delete_line(&DeleteLine, window, cx);
23463 editor.move_up(&MoveUp, window, cx);
23464 editor.delete_line(&DeleteLine, window, cx);
23465 });
23466 executor.run_until_parked();
23467 cx.assert_state_with_diff(
23468 r#"
23469 use some::mod1;
23470 use some::mod2;
23471
23472 const A: u32 = 42;
23473 + const B: u32 = 42;
23474 ˇ
23475 fn main() {
23476 println!("hello");
23477
23478 println!("world");
23479 }
23480 "#
23481 .unindent(),
23482 );
23483
23484 cx.update_editor(|editor, window, cx| {
23485 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
23486 editor.delete_line(&DeleteLine, window, cx);
23487 });
23488 executor.run_until_parked();
23489 cx.assert_state_with_diff(
23490 r#"
23491 ˇ
23492 fn main() {
23493 println!("hello");
23494
23495 println!("world");
23496 }
23497 "#
23498 .unindent(),
23499 );
23500}
23501
23502#[gpui::test]
23503async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
23504 init_test(cx, |_| {});
23505
23506 let mut cx = EditorTestContext::new(cx).await;
23507 cx.set_head_text(indoc! { "
23508 one
23509 two
23510 three
23511 four
23512 five
23513 "
23514 });
23515 cx.set_state(indoc! { "
23516 one
23517 ˇthree
23518 five
23519 "});
23520 cx.run_until_parked();
23521 cx.update_editor(|editor, window, cx| {
23522 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23523 });
23524 cx.assert_state_with_diff(
23525 indoc! { "
23526 one
23527 - two
23528 ˇthree
23529 - four
23530 five
23531 "}
23532 .to_string(),
23533 );
23534 cx.update_editor(|editor, window, cx| {
23535 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23536 });
23537
23538 cx.assert_state_with_diff(
23539 indoc! { "
23540 one
23541 ˇthree
23542 five
23543 "}
23544 .to_string(),
23545 );
23546
23547 cx.update_editor(|editor, window, cx| {
23548 editor.move_up(&MoveUp, window, cx);
23549 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23550 });
23551 cx.assert_state_with_diff(
23552 indoc! { "
23553 ˇone
23554 - two
23555 three
23556 five
23557 "}
23558 .to_string(),
23559 );
23560
23561 cx.update_editor(|editor, window, cx| {
23562 editor.move_down(&MoveDown, window, cx);
23563 editor.move_down(&MoveDown, window, cx);
23564 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23565 });
23566 cx.assert_state_with_diff(
23567 indoc! { "
23568 one
23569 - two
23570 ˇthree
23571 - four
23572 five
23573 "}
23574 .to_string(),
23575 );
23576
23577 cx.set_state(indoc! { "
23578 one
23579 ˇTWO
23580 three
23581 four
23582 five
23583 "});
23584 cx.run_until_parked();
23585 cx.update_editor(|editor, window, cx| {
23586 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23587 });
23588
23589 cx.assert_state_with_diff(
23590 indoc! { "
23591 one
23592 - two
23593 + ˇTWO
23594 three
23595 four
23596 five
23597 "}
23598 .to_string(),
23599 );
23600 cx.update_editor(|editor, window, cx| {
23601 editor.move_up(&Default::default(), window, cx);
23602 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23603 });
23604 cx.assert_state_with_diff(
23605 indoc! { "
23606 one
23607 ˇTWO
23608 three
23609 four
23610 five
23611 "}
23612 .to_string(),
23613 );
23614}
23615
23616#[gpui::test]
23617async fn test_toggling_adjacent_diff_hunks_2(
23618 executor: BackgroundExecutor,
23619 cx: &mut TestAppContext,
23620) {
23621 init_test(cx, |_| {});
23622
23623 let mut cx = EditorTestContext::new(cx).await;
23624
23625 let diff_base = r#"
23626 lineA
23627 lineB
23628 lineC
23629 lineD
23630 "#
23631 .unindent();
23632
23633 cx.set_state(
23634 &r#"
23635 ˇlineA1
23636 lineB
23637 lineD
23638 "#
23639 .unindent(),
23640 );
23641 cx.set_head_text(&diff_base);
23642 executor.run_until_parked();
23643
23644 cx.update_editor(|editor, window, cx| {
23645 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23646 });
23647 executor.run_until_parked();
23648 cx.assert_state_with_diff(
23649 r#"
23650 - lineA
23651 + ˇlineA1
23652 lineB
23653 lineD
23654 "#
23655 .unindent(),
23656 );
23657
23658 cx.update_editor(|editor, window, cx| {
23659 editor.move_down(&MoveDown, window, cx);
23660 editor.move_right(&MoveRight, window, cx);
23661 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23662 });
23663 executor.run_until_parked();
23664 cx.assert_state_with_diff(
23665 r#"
23666 - lineA
23667 + lineA1
23668 lˇineB
23669 - lineC
23670 lineD
23671 "#
23672 .unindent(),
23673 );
23674}
23675
23676#[gpui::test]
23677async fn test_edits_around_expanded_deletion_hunks(
23678 executor: BackgroundExecutor,
23679 cx: &mut TestAppContext,
23680) {
23681 init_test(cx, |_| {});
23682
23683 let mut cx = EditorTestContext::new(cx).await;
23684
23685 let diff_base = r#"
23686 use some::mod1;
23687 use some::mod2;
23688
23689 const A: u32 = 42;
23690 const B: u32 = 42;
23691 const C: u32 = 42;
23692
23693
23694 fn main() {
23695 println!("hello");
23696
23697 println!("world");
23698 }
23699 "#
23700 .unindent();
23701 executor.run_until_parked();
23702 cx.set_state(
23703 &r#"
23704 use some::mod1;
23705 use some::mod2;
23706
23707 ˇconst B: u32 = 42;
23708 const C: u32 = 42;
23709
23710
23711 fn main() {
23712 println!("hello");
23713
23714 println!("world");
23715 }
23716 "#
23717 .unindent(),
23718 );
23719
23720 cx.set_head_text(&diff_base);
23721 executor.run_until_parked();
23722
23723 cx.update_editor(|editor, window, cx| {
23724 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23725 });
23726 executor.run_until_parked();
23727
23728 cx.assert_state_with_diff(
23729 r#"
23730 use some::mod1;
23731 use some::mod2;
23732
23733 - const A: u32 = 42;
23734 ˇconst B: u32 = 42;
23735 const C: u32 = 42;
23736
23737
23738 fn main() {
23739 println!("hello");
23740
23741 println!("world");
23742 }
23743 "#
23744 .unindent(),
23745 );
23746
23747 cx.update_editor(|editor, window, cx| {
23748 editor.delete_line(&DeleteLine, window, cx);
23749 });
23750 executor.run_until_parked();
23751 cx.assert_state_with_diff(
23752 r#"
23753 use some::mod1;
23754 use some::mod2;
23755
23756 - const A: u32 = 42;
23757 - const B: u32 = 42;
23758 ˇconst C: u32 = 42;
23759
23760
23761 fn main() {
23762 println!("hello");
23763
23764 println!("world");
23765 }
23766 "#
23767 .unindent(),
23768 );
23769
23770 cx.update_editor(|editor, window, cx| {
23771 editor.delete_line(&DeleteLine, window, cx);
23772 });
23773 executor.run_until_parked();
23774 cx.assert_state_with_diff(
23775 r#"
23776 use some::mod1;
23777 use some::mod2;
23778
23779 - const A: u32 = 42;
23780 - const B: u32 = 42;
23781 - const C: u32 = 42;
23782 ˇ
23783
23784 fn main() {
23785 println!("hello");
23786
23787 println!("world");
23788 }
23789 "#
23790 .unindent(),
23791 );
23792
23793 cx.update_editor(|editor, window, cx| {
23794 editor.handle_input("replacement", window, cx);
23795 });
23796 executor.run_until_parked();
23797 cx.assert_state_with_diff(
23798 r#"
23799 use some::mod1;
23800 use some::mod2;
23801
23802 - const A: u32 = 42;
23803 - const B: u32 = 42;
23804 - const C: u32 = 42;
23805 -
23806 + replacementˇ
23807
23808 fn main() {
23809 println!("hello");
23810
23811 println!("world");
23812 }
23813 "#
23814 .unindent(),
23815 );
23816}
23817
23818#[gpui::test]
23819async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
23820 init_test(cx, |_| {});
23821
23822 let mut cx = EditorTestContext::new(cx).await;
23823
23824 let base_text = r#"
23825 one
23826 two
23827 three
23828 four
23829 five
23830 "#
23831 .unindent();
23832 executor.run_until_parked();
23833 cx.set_state(
23834 &r#"
23835 one
23836 two
23837 fˇour
23838 five
23839 "#
23840 .unindent(),
23841 );
23842
23843 cx.set_head_text(&base_text);
23844 executor.run_until_parked();
23845
23846 cx.update_editor(|editor, window, cx| {
23847 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23848 });
23849 executor.run_until_parked();
23850
23851 cx.assert_state_with_diff(
23852 r#"
23853 one
23854 two
23855 - three
23856 fˇour
23857 five
23858 "#
23859 .unindent(),
23860 );
23861
23862 cx.update_editor(|editor, window, cx| {
23863 editor.backspace(&Backspace, window, cx);
23864 editor.backspace(&Backspace, window, cx);
23865 });
23866 executor.run_until_parked();
23867 cx.assert_state_with_diff(
23868 r#"
23869 one
23870 two
23871 - threeˇ
23872 - four
23873 + our
23874 five
23875 "#
23876 .unindent(),
23877 );
23878}
23879
23880#[gpui::test]
23881async fn test_edit_after_expanded_modification_hunk(
23882 executor: BackgroundExecutor,
23883 cx: &mut TestAppContext,
23884) {
23885 init_test(cx, |_| {});
23886
23887 let mut cx = EditorTestContext::new(cx).await;
23888
23889 let diff_base = r#"
23890 use some::mod1;
23891 use some::mod2;
23892
23893 const A: u32 = 42;
23894 const B: u32 = 42;
23895 const C: u32 = 42;
23896 const D: u32 = 42;
23897
23898
23899 fn main() {
23900 println!("hello");
23901
23902 println!("world");
23903 }"#
23904 .unindent();
23905
23906 cx.set_state(
23907 &r#"
23908 use some::mod1;
23909 use some::mod2;
23910
23911 const A: u32 = 42;
23912 const B: u32 = 42;
23913 const C: u32 = 43ˇ
23914 const D: u32 = 42;
23915
23916
23917 fn main() {
23918 println!("hello");
23919
23920 println!("world");
23921 }"#
23922 .unindent(),
23923 );
23924
23925 cx.set_head_text(&diff_base);
23926 executor.run_until_parked();
23927 cx.update_editor(|editor, window, cx| {
23928 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23929 });
23930 executor.run_until_parked();
23931
23932 cx.assert_state_with_diff(
23933 r#"
23934 use some::mod1;
23935 use some::mod2;
23936
23937 const A: u32 = 42;
23938 const B: u32 = 42;
23939 - const C: u32 = 42;
23940 + const C: u32 = 43ˇ
23941 const D: u32 = 42;
23942
23943
23944 fn main() {
23945 println!("hello");
23946
23947 println!("world");
23948 }"#
23949 .unindent(),
23950 );
23951
23952 cx.update_editor(|editor, window, cx| {
23953 editor.handle_input("\nnew_line\n", window, cx);
23954 });
23955 executor.run_until_parked();
23956
23957 cx.assert_state_with_diff(
23958 r#"
23959 use some::mod1;
23960 use some::mod2;
23961
23962 const A: u32 = 42;
23963 const B: u32 = 42;
23964 - const C: u32 = 42;
23965 + const C: u32 = 43
23966 + new_line
23967 + ˇ
23968 const D: u32 = 42;
23969
23970
23971 fn main() {
23972 println!("hello");
23973
23974 println!("world");
23975 }"#
23976 .unindent(),
23977 );
23978}
23979
23980#[gpui::test]
23981async fn test_stage_and_unstage_added_file_hunk(
23982 executor: BackgroundExecutor,
23983 cx: &mut TestAppContext,
23984) {
23985 init_test(cx, |_| {});
23986
23987 let mut cx = EditorTestContext::new(cx).await;
23988 cx.update_editor(|editor, _, cx| {
23989 editor.set_expand_all_diff_hunks(cx);
23990 });
23991
23992 let working_copy = r#"
23993 ˇfn main() {
23994 println!("hello, world!");
23995 }
23996 "#
23997 .unindent();
23998
23999 cx.set_state(&working_copy);
24000 executor.run_until_parked();
24001
24002 cx.assert_state_with_diff(
24003 r#"
24004 + ˇfn main() {
24005 + println!("hello, world!");
24006 + }
24007 "#
24008 .unindent(),
24009 );
24010 cx.assert_index_text(None);
24011
24012 cx.update_editor(|editor, window, cx| {
24013 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
24014 });
24015 executor.run_until_parked();
24016 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
24017 cx.assert_state_with_diff(
24018 r#"
24019 + ˇfn main() {
24020 + println!("hello, world!");
24021 + }
24022 "#
24023 .unindent(),
24024 );
24025
24026 cx.update_editor(|editor, window, cx| {
24027 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
24028 });
24029 executor.run_until_parked();
24030 cx.assert_index_text(None);
24031}
24032
24033async fn setup_indent_guides_editor(
24034 text: &str,
24035 cx: &mut TestAppContext,
24036) -> (BufferId, EditorTestContext) {
24037 init_test(cx, |_| {});
24038
24039 let mut cx = EditorTestContext::new(cx).await;
24040
24041 let buffer_id = cx.update_editor(|editor, window, cx| {
24042 editor.set_text(text, window, cx);
24043 editor
24044 .buffer()
24045 .read(cx)
24046 .as_singleton()
24047 .unwrap()
24048 .read(cx)
24049 .remote_id()
24050 });
24051
24052 (buffer_id, cx)
24053}
24054
24055fn assert_indent_guides(
24056 range: Range<u32>,
24057 expected: Vec<IndentGuide>,
24058 active_indices: Option<Vec<usize>>,
24059 cx: &mut EditorTestContext,
24060) {
24061 let indent_guides = cx.update_editor(|editor, window, cx| {
24062 let snapshot = editor.snapshot(window, cx).display_snapshot;
24063 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
24064 editor,
24065 MultiBufferRow(range.start)..MultiBufferRow(range.end),
24066 true,
24067 &snapshot,
24068 cx,
24069 );
24070
24071 indent_guides.sort_by(|a, b| {
24072 a.depth.cmp(&b.depth).then(
24073 a.start_row
24074 .cmp(&b.start_row)
24075 .then(a.end_row.cmp(&b.end_row)),
24076 )
24077 });
24078 indent_guides
24079 });
24080
24081 if let Some(expected) = active_indices {
24082 let active_indices = cx.update_editor(|editor, window, cx| {
24083 let snapshot = editor.snapshot(window, cx).display_snapshot;
24084 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
24085 });
24086
24087 assert_eq!(
24088 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
24089 expected,
24090 "Active indent guide indices do not match"
24091 );
24092 }
24093
24094 assert_eq!(indent_guides, expected, "Indent guides do not match");
24095}
24096
24097fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
24098 IndentGuide {
24099 buffer_id,
24100 start_row: MultiBufferRow(start_row),
24101 end_row: MultiBufferRow(end_row),
24102 depth,
24103 tab_size: 4,
24104 settings: IndentGuideSettings {
24105 enabled: true,
24106 line_width: 1,
24107 active_line_width: 1,
24108 coloring: IndentGuideColoring::default(),
24109 background_coloring: IndentGuideBackgroundColoring::default(),
24110 },
24111 }
24112}
24113
24114#[gpui::test]
24115async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
24116 let (buffer_id, mut cx) = setup_indent_guides_editor(
24117 &"
24118 fn main() {
24119 let a = 1;
24120 }"
24121 .unindent(),
24122 cx,
24123 )
24124 .await;
24125
24126 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
24127}
24128
24129#[gpui::test]
24130async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
24131 let (buffer_id, mut cx) = setup_indent_guides_editor(
24132 &"
24133 fn main() {
24134 let a = 1;
24135 let b = 2;
24136 }"
24137 .unindent(),
24138 cx,
24139 )
24140 .await;
24141
24142 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
24143}
24144
24145#[gpui::test]
24146async fn test_indent_guide_nested(cx: &mut TestAppContext) {
24147 let (buffer_id, mut cx) = setup_indent_guides_editor(
24148 &"
24149 fn main() {
24150 let a = 1;
24151 if a == 3 {
24152 let b = 2;
24153 } else {
24154 let c = 3;
24155 }
24156 }"
24157 .unindent(),
24158 cx,
24159 )
24160 .await;
24161
24162 assert_indent_guides(
24163 0..8,
24164 vec![
24165 indent_guide(buffer_id, 1, 6, 0),
24166 indent_guide(buffer_id, 3, 3, 1),
24167 indent_guide(buffer_id, 5, 5, 1),
24168 ],
24169 None,
24170 &mut cx,
24171 );
24172}
24173
24174#[gpui::test]
24175async fn test_indent_guide_tab(cx: &mut TestAppContext) {
24176 let (buffer_id, mut cx) = setup_indent_guides_editor(
24177 &"
24178 fn main() {
24179 let a = 1;
24180 let b = 2;
24181 let c = 3;
24182 }"
24183 .unindent(),
24184 cx,
24185 )
24186 .await;
24187
24188 assert_indent_guides(
24189 0..5,
24190 vec![
24191 indent_guide(buffer_id, 1, 3, 0),
24192 indent_guide(buffer_id, 2, 2, 1),
24193 ],
24194 None,
24195 &mut cx,
24196 );
24197}
24198
24199#[gpui::test]
24200async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
24201 let (buffer_id, mut cx) = setup_indent_guides_editor(
24202 &"
24203 fn main() {
24204 let a = 1;
24205
24206 let c = 3;
24207 }"
24208 .unindent(),
24209 cx,
24210 )
24211 .await;
24212
24213 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
24214}
24215
24216#[gpui::test]
24217async fn test_indent_guide_complex(cx: &mut TestAppContext) {
24218 let (buffer_id, mut cx) = setup_indent_guides_editor(
24219 &"
24220 fn main() {
24221 let a = 1;
24222
24223 let c = 3;
24224
24225 if a == 3 {
24226 let b = 2;
24227 } else {
24228 let c = 3;
24229 }
24230 }"
24231 .unindent(),
24232 cx,
24233 )
24234 .await;
24235
24236 assert_indent_guides(
24237 0..11,
24238 vec![
24239 indent_guide(buffer_id, 1, 9, 0),
24240 indent_guide(buffer_id, 6, 6, 1),
24241 indent_guide(buffer_id, 8, 8, 1),
24242 ],
24243 None,
24244 &mut cx,
24245 );
24246}
24247
24248#[gpui::test]
24249async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
24250 let (buffer_id, mut cx) = setup_indent_guides_editor(
24251 &"
24252 fn main() {
24253 let a = 1;
24254
24255 let c = 3;
24256
24257 if a == 3 {
24258 let b = 2;
24259 } else {
24260 let c = 3;
24261 }
24262 }"
24263 .unindent(),
24264 cx,
24265 )
24266 .await;
24267
24268 assert_indent_guides(
24269 1..11,
24270 vec![
24271 indent_guide(buffer_id, 1, 9, 0),
24272 indent_guide(buffer_id, 6, 6, 1),
24273 indent_guide(buffer_id, 8, 8, 1),
24274 ],
24275 None,
24276 &mut cx,
24277 );
24278}
24279
24280#[gpui::test]
24281async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
24282 let (buffer_id, mut cx) = setup_indent_guides_editor(
24283 &"
24284 fn main() {
24285 let a = 1;
24286
24287 let c = 3;
24288
24289 if a == 3 {
24290 let b = 2;
24291 } else {
24292 let c = 3;
24293 }
24294 }"
24295 .unindent(),
24296 cx,
24297 )
24298 .await;
24299
24300 assert_indent_guides(
24301 1..10,
24302 vec![
24303 indent_guide(buffer_id, 1, 9, 0),
24304 indent_guide(buffer_id, 6, 6, 1),
24305 indent_guide(buffer_id, 8, 8, 1),
24306 ],
24307 None,
24308 &mut cx,
24309 );
24310}
24311
24312#[gpui::test]
24313async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
24314 let (buffer_id, mut cx) = setup_indent_guides_editor(
24315 &"
24316 fn main() {
24317 if a {
24318 b(
24319 c,
24320 d,
24321 )
24322 } else {
24323 e(
24324 f
24325 )
24326 }
24327 }"
24328 .unindent(),
24329 cx,
24330 )
24331 .await;
24332
24333 assert_indent_guides(
24334 0..11,
24335 vec![
24336 indent_guide(buffer_id, 1, 10, 0),
24337 indent_guide(buffer_id, 2, 5, 1),
24338 indent_guide(buffer_id, 7, 9, 1),
24339 indent_guide(buffer_id, 3, 4, 2),
24340 indent_guide(buffer_id, 8, 8, 2),
24341 ],
24342 None,
24343 &mut cx,
24344 );
24345
24346 cx.update_editor(|editor, window, cx| {
24347 editor.fold_at(MultiBufferRow(2), window, cx);
24348 assert_eq!(
24349 editor.display_text(cx),
24350 "
24351 fn main() {
24352 if a {
24353 b(⋯)
24354 } else {
24355 e(
24356 f
24357 )
24358 }
24359 }"
24360 .unindent()
24361 );
24362 });
24363
24364 assert_indent_guides(
24365 0..11,
24366 vec![
24367 indent_guide(buffer_id, 1, 10, 0),
24368 indent_guide(buffer_id, 2, 5, 1),
24369 indent_guide(buffer_id, 7, 9, 1),
24370 indent_guide(buffer_id, 8, 8, 2),
24371 ],
24372 None,
24373 &mut cx,
24374 );
24375}
24376
24377#[gpui::test]
24378async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
24379 let (buffer_id, mut cx) = setup_indent_guides_editor(
24380 &"
24381 block1
24382 block2
24383 block3
24384 block4
24385 block2
24386 block1
24387 block1"
24388 .unindent(),
24389 cx,
24390 )
24391 .await;
24392
24393 assert_indent_guides(
24394 1..10,
24395 vec![
24396 indent_guide(buffer_id, 1, 4, 0),
24397 indent_guide(buffer_id, 2, 3, 1),
24398 indent_guide(buffer_id, 3, 3, 2),
24399 ],
24400 None,
24401 &mut cx,
24402 );
24403}
24404
24405#[gpui::test]
24406async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
24407 let (buffer_id, mut cx) = setup_indent_guides_editor(
24408 &"
24409 block1
24410 block2
24411 block3
24412
24413 block1
24414 block1"
24415 .unindent(),
24416 cx,
24417 )
24418 .await;
24419
24420 assert_indent_guides(
24421 0..6,
24422 vec![
24423 indent_guide(buffer_id, 1, 2, 0),
24424 indent_guide(buffer_id, 2, 2, 1),
24425 ],
24426 None,
24427 &mut cx,
24428 );
24429}
24430
24431#[gpui::test]
24432async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
24433 let (buffer_id, mut cx) = setup_indent_guides_editor(
24434 &"
24435 function component() {
24436 \treturn (
24437 \t\t\t
24438 \t\t<div>
24439 \t\t\t<abc></abc>
24440 \t\t</div>
24441 \t)
24442 }"
24443 .unindent(),
24444 cx,
24445 )
24446 .await;
24447
24448 assert_indent_guides(
24449 0..8,
24450 vec![
24451 indent_guide(buffer_id, 1, 6, 0),
24452 indent_guide(buffer_id, 2, 5, 1),
24453 indent_guide(buffer_id, 4, 4, 2),
24454 ],
24455 None,
24456 &mut cx,
24457 );
24458}
24459
24460#[gpui::test]
24461async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
24462 let (buffer_id, mut cx) = setup_indent_guides_editor(
24463 &"
24464 function component() {
24465 \treturn (
24466 \t
24467 \t\t<div>
24468 \t\t\t<abc></abc>
24469 \t\t</div>
24470 \t)
24471 }"
24472 .unindent(),
24473 cx,
24474 )
24475 .await;
24476
24477 assert_indent_guides(
24478 0..8,
24479 vec![
24480 indent_guide(buffer_id, 1, 6, 0),
24481 indent_guide(buffer_id, 2, 5, 1),
24482 indent_guide(buffer_id, 4, 4, 2),
24483 ],
24484 None,
24485 &mut cx,
24486 );
24487}
24488
24489#[gpui::test]
24490async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
24491 let (buffer_id, mut cx) = setup_indent_guides_editor(
24492 &"
24493 block1
24494
24495
24496
24497 block2
24498 "
24499 .unindent(),
24500 cx,
24501 )
24502 .await;
24503
24504 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
24505}
24506
24507#[gpui::test]
24508async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
24509 let (buffer_id, mut cx) = setup_indent_guides_editor(
24510 &"
24511 def a:
24512 \tb = 3
24513 \tif True:
24514 \t\tc = 4
24515 \t\td = 5
24516 \tprint(b)
24517 "
24518 .unindent(),
24519 cx,
24520 )
24521 .await;
24522
24523 assert_indent_guides(
24524 0..6,
24525 vec![
24526 indent_guide(buffer_id, 1, 5, 0),
24527 indent_guide(buffer_id, 3, 4, 1),
24528 ],
24529 None,
24530 &mut cx,
24531 );
24532}
24533
24534#[gpui::test]
24535async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
24536 let (buffer_id, mut cx) = setup_indent_guides_editor(
24537 &"
24538 fn main() {
24539 let a = 1;
24540 }"
24541 .unindent(),
24542 cx,
24543 )
24544 .await;
24545
24546 cx.update_editor(|editor, window, cx| {
24547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24548 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
24549 });
24550 });
24551
24552 assert_indent_guides(
24553 0..3,
24554 vec![indent_guide(buffer_id, 1, 1, 0)],
24555 Some(vec![0]),
24556 &mut cx,
24557 );
24558}
24559
24560#[gpui::test]
24561async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
24562 let (buffer_id, mut cx) = setup_indent_guides_editor(
24563 &"
24564 fn main() {
24565 if 1 == 2 {
24566 let a = 1;
24567 }
24568 }"
24569 .unindent(),
24570 cx,
24571 )
24572 .await;
24573
24574 cx.update_editor(|editor, window, cx| {
24575 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24576 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
24577 });
24578 });
24579 cx.run_until_parked();
24580
24581 assert_indent_guides(
24582 0..4,
24583 vec![
24584 indent_guide(buffer_id, 1, 3, 0),
24585 indent_guide(buffer_id, 2, 2, 1),
24586 ],
24587 Some(vec![1]),
24588 &mut cx,
24589 );
24590
24591 cx.update_editor(|editor, window, cx| {
24592 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24593 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
24594 });
24595 });
24596 cx.run_until_parked();
24597
24598 assert_indent_guides(
24599 0..4,
24600 vec![
24601 indent_guide(buffer_id, 1, 3, 0),
24602 indent_guide(buffer_id, 2, 2, 1),
24603 ],
24604 Some(vec![1]),
24605 &mut cx,
24606 );
24607
24608 cx.update_editor(|editor, window, cx| {
24609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24610 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
24611 });
24612 });
24613 cx.run_until_parked();
24614
24615 assert_indent_guides(
24616 0..4,
24617 vec![
24618 indent_guide(buffer_id, 1, 3, 0),
24619 indent_guide(buffer_id, 2, 2, 1),
24620 ],
24621 Some(vec![0]),
24622 &mut cx,
24623 );
24624}
24625
24626#[gpui::test]
24627async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
24628 let (buffer_id, mut cx) = setup_indent_guides_editor(
24629 &"
24630 fn main() {
24631 let a = 1;
24632
24633 let b = 2;
24634 }"
24635 .unindent(),
24636 cx,
24637 )
24638 .await;
24639
24640 cx.update_editor(|editor, window, cx| {
24641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24642 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
24643 });
24644 });
24645
24646 assert_indent_guides(
24647 0..5,
24648 vec![indent_guide(buffer_id, 1, 3, 0)],
24649 Some(vec![0]),
24650 &mut cx,
24651 );
24652}
24653
24654#[gpui::test]
24655async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
24656 let (buffer_id, mut cx) = setup_indent_guides_editor(
24657 &"
24658 def m:
24659 a = 1
24660 pass"
24661 .unindent(),
24662 cx,
24663 )
24664 .await;
24665
24666 cx.update_editor(|editor, window, cx| {
24667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24668 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
24669 });
24670 });
24671
24672 assert_indent_guides(
24673 0..3,
24674 vec![indent_guide(buffer_id, 1, 2, 0)],
24675 Some(vec![0]),
24676 &mut cx,
24677 );
24678}
24679
24680#[gpui::test]
24681async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
24682 init_test(cx, |_| {});
24683 let mut cx = EditorTestContext::new(cx).await;
24684 let text = indoc! {
24685 "
24686 impl A {
24687 fn b() {
24688 0;
24689 3;
24690 5;
24691 6;
24692 7;
24693 }
24694 }
24695 "
24696 };
24697 let base_text = indoc! {
24698 "
24699 impl A {
24700 fn b() {
24701 0;
24702 1;
24703 2;
24704 3;
24705 4;
24706 }
24707 fn c() {
24708 5;
24709 6;
24710 7;
24711 }
24712 }
24713 "
24714 };
24715
24716 cx.update_editor(|editor, window, cx| {
24717 editor.set_text(text, window, cx);
24718
24719 editor.buffer().update(cx, |multibuffer, cx| {
24720 let buffer = multibuffer.as_singleton().unwrap();
24721 let diff = cx.new(|cx| {
24722 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
24723 });
24724
24725 multibuffer.set_all_diff_hunks_expanded(cx);
24726 multibuffer.add_diff(diff, cx);
24727
24728 buffer.read(cx).remote_id()
24729 })
24730 });
24731 cx.run_until_parked();
24732
24733 cx.assert_state_with_diff(
24734 indoc! { "
24735 impl A {
24736 fn b() {
24737 0;
24738 - 1;
24739 - 2;
24740 3;
24741 - 4;
24742 - }
24743 - fn c() {
24744 5;
24745 6;
24746 7;
24747 }
24748 }
24749 ˇ"
24750 }
24751 .to_string(),
24752 );
24753
24754 let mut actual_guides = cx.update_editor(|editor, window, cx| {
24755 editor
24756 .snapshot(window, cx)
24757 .buffer_snapshot()
24758 .indent_guides_in_range(Anchor::Min..Anchor::Max, false, cx)
24759 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
24760 .collect::<Vec<_>>()
24761 });
24762 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
24763 assert_eq!(
24764 actual_guides,
24765 vec![
24766 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
24767 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
24768 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
24769 ]
24770 );
24771}
24772
24773#[gpui::test]
24774async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
24775 init_test(cx, |_| {});
24776 let mut cx = EditorTestContext::new(cx).await;
24777
24778 let diff_base = r#"
24779 a
24780 b
24781 c
24782 "#
24783 .unindent();
24784
24785 cx.set_state(
24786 &r#"
24787 ˇA
24788 b
24789 C
24790 "#
24791 .unindent(),
24792 );
24793 cx.set_head_text(&diff_base);
24794 cx.update_editor(|editor, window, cx| {
24795 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24796 });
24797 executor.run_until_parked();
24798
24799 let both_hunks_expanded = r#"
24800 - a
24801 + ˇA
24802 b
24803 - c
24804 + C
24805 "#
24806 .unindent();
24807
24808 cx.assert_state_with_diff(both_hunks_expanded.clone());
24809
24810 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24811 let snapshot = editor.snapshot(window, cx);
24812 let hunks = editor
24813 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24814 .collect::<Vec<_>>();
24815 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
24816 hunks
24817 .into_iter()
24818 .map(|hunk| {
24819 multibuffer_snapshot
24820 .anchor_in_excerpt(hunk.buffer_range.start)
24821 .unwrap()
24822 ..multibuffer_snapshot
24823 .anchor_in_excerpt(hunk.buffer_range.end)
24824 .unwrap()
24825 })
24826 .collect::<Vec<_>>()
24827 });
24828 assert_eq!(hunk_ranges.len(), 2);
24829
24830 cx.update_editor(|editor, _, cx| {
24831 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24832 });
24833 executor.run_until_parked();
24834
24835 let second_hunk_expanded = r#"
24836 ˇA
24837 b
24838 - c
24839 + C
24840 "#
24841 .unindent();
24842
24843 cx.assert_state_with_diff(second_hunk_expanded);
24844
24845 cx.update_editor(|editor, _, cx| {
24846 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24847 });
24848 executor.run_until_parked();
24849
24850 cx.assert_state_with_diff(both_hunks_expanded.clone());
24851
24852 cx.update_editor(|editor, _, cx| {
24853 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24854 });
24855 executor.run_until_parked();
24856
24857 let first_hunk_expanded = r#"
24858 - a
24859 + ˇA
24860 b
24861 C
24862 "#
24863 .unindent();
24864
24865 cx.assert_state_with_diff(first_hunk_expanded);
24866
24867 cx.update_editor(|editor, _, cx| {
24868 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24869 });
24870 executor.run_until_parked();
24871
24872 cx.assert_state_with_diff(both_hunks_expanded);
24873
24874 cx.set_state(
24875 &r#"
24876 ˇA
24877 b
24878 "#
24879 .unindent(),
24880 );
24881 cx.run_until_parked();
24882
24883 // TODO this cursor position seems bad
24884 cx.assert_state_with_diff(
24885 r#"
24886 - ˇa
24887 + A
24888 b
24889 "#
24890 .unindent(),
24891 );
24892
24893 cx.update_editor(|editor, window, cx| {
24894 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24895 });
24896
24897 cx.assert_state_with_diff(
24898 r#"
24899 - ˇa
24900 + A
24901 b
24902 - c
24903 "#
24904 .unindent(),
24905 );
24906
24907 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24908 let snapshot = editor.snapshot(window, cx);
24909 let hunks = editor
24910 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24911 .collect::<Vec<_>>();
24912 let multibuffer_snapshot = snapshot.buffer_snapshot();
24913 hunks
24914 .into_iter()
24915 .map(|hunk| {
24916 multibuffer_snapshot
24917 .anchor_in_excerpt(hunk.buffer_range.start)
24918 .unwrap()
24919 ..multibuffer_snapshot
24920 .anchor_in_excerpt(hunk.buffer_range.end)
24921 .unwrap()
24922 })
24923 .collect::<Vec<_>>()
24924 });
24925 assert_eq!(hunk_ranges.len(), 2);
24926
24927 cx.update_editor(|editor, _, cx| {
24928 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24929 });
24930 executor.run_until_parked();
24931
24932 cx.assert_state_with_diff(
24933 r#"
24934 - ˇa
24935 + A
24936 b
24937 "#
24938 .unindent(),
24939 );
24940}
24941
24942#[gpui::test]
24943async fn test_toggle_deletion_hunk_at_start_of_file(
24944 executor: BackgroundExecutor,
24945 cx: &mut TestAppContext,
24946) {
24947 init_test(cx, |_| {});
24948 let mut cx = EditorTestContext::new(cx).await;
24949
24950 let diff_base = r#"
24951 a
24952 b
24953 c
24954 "#
24955 .unindent();
24956
24957 cx.set_state(
24958 &r#"
24959 ˇb
24960 c
24961 "#
24962 .unindent(),
24963 );
24964 cx.set_head_text(&diff_base);
24965 cx.update_editor(|editor, window, cx| {
24966 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24967 });
24968 executor.run_until_parked();
24969
24970 let hunk_expanded = r#"
24971 - a
24972 ˇb
24973 c
24974 "#
24975 .unindent();
24976
24977 cx.assert_state_with_diff(hunk_expanded.clone());
24978
24979 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24980 let snapshot = editor.snapshot(window, cx);
24981 let hunks = editor
24982 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24983 .collect::<Vec<_>>();
24984 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
24985 hunks
24986 .into_iter()
24987 .map(|hunk| {
24988 multibuffer_snapshot
24989 .anchor_in_excerpt(hunk.buffer_range.start)
24990 .unwrap()
24991 ..multibuffer_snapshot
24992 .anchor_in_excerpt(hunk.buffer_range.end)
24993 .unwrap()
24994 })
24995 .collect::<Vec<_>>()
24996 });
24997 assert_eq!(hunk_ranges.len(), 1);
24998
24999 cx.update_editor(|editor, _, cx| {
25000 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
25001 });
25002 executor.run_until_parked();
25003
25004 let hunk_collapsed = r#"
25005 ˇb
25006 c
25007 "#
25008 .unindent();
25009
25010 cx.assert_state_with_diff(hunk_collapsed);
25011
25012 cx.update_editor(|editor, _, cx| {
25013 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
25014 });
25015 executor.run_until_parked();
25016
25017 cx.assert_state_with_diff(hunk_expanded);
25018}
25019
25020#[gpui::test]
25021async fn test_select_smaller_syntax_node_after_diff_hunk_collapse(
25022 executor: BackgroundExecutor,
25023 cx: &mut TestAppContext,
25024) {
25025 init_test(cx, |_| {});
25026
25027 let mut cx = EditorTestContext::new(cx).await;
25028 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
25029
25030 cx.set_state(
25031 &r#"
25032 fn main() {
25033 let x = ˇ1;
25034 }
25035 "#
25036 .unindent(),
25037 );
25038
25039 let diff_base = r#"
25040 fn removed_one() {
25041 println!("this function was deleted");
25042 }
25043
25044 fn removed_two() {
25045 println!("this function was also deleted");
25046 }
25047
25048 fn main() {
25049 let x = 1;
25050 }
25051 "#
25052 .unindent();
25053 cx.set_head_text(&diff_base);
25054 executor.run_until_parked();
25055
25056 cx.update_editor(|editor, window, cx| {
25057 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
25058 });
25059 executor.run_until_parked();
25060
25061 cx.update_editor(|editor, window, cx| {
25062 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
25063 });
25064
25065 cx.update_editor(|editor, window, cx| {
25066 editor.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
25067 });
25068 executor.run_until_parked();
25069
25070 cx.update_editor(|editor, window, cx| {
25071 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
25072 });
25073}
25074
25075#[gpui::test]
25076async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
25077 executor: BackgroundExecutor,
25078 cx: &mut TestAppContext,
25079) {
25080 init_test(cx, |_| {});
25081 let mut cx = EditorTestContext::new(cx).await;
25082
25083 cx.set_state("ˇnew\nsecond\nthird\n");
25084 cx.set_head_text("old\nsecond\nthird\n");
25085 cx.update_editor(|editor, window, cx| {
25086 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
25087 });
25088 executor.run_until_parked();
25089 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
25090
25091 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
25092 cx.update_editor(|editor, window, cx| {
25093 let snapshot = editor.snapshot(window, cx);
25094 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
25095 let hunks = editor
25096 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
25097 .collect::<Vec<_>>();
25098 assert_eq!(hunks.len(), 1);
25099 let hunk_range = multibuffer_snapshot
25100 .anchor_in_excerpt(hunks[0].buffer_range.start)
25101 .unwrap()
25102 ..multibuffer_snapshot
25103 .anchor_in_excerpt(hunks[0].buffer_range.end)
25104 .unwrap();
25105 editor.toggle_single_diff_hunk(hunk_range, cx)
25106 });
25107 executor.run_until_parked();
25108 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
25109
25110 // Keep the editor scrolled to the top so the full hunk remains visible.
25111 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
25112}
25113
25114#[gpui::test]
25115async fn test_display_diff_hunks(cx: &mut TestAppContext) {
25116 init_test(cx, |_| {});
25117
25118 let fs = FakeFs::new(cx.executor());
25119 fs.insert_tree(
25120 path!("/test"),
25121 json!({
25122 ".git": {},
25123 "file-1": "ONE\n",
25124 "file-2": "TWO\n",
25125 "file-3": "THREE\n",
25126 }),
25127 )
25128 .await;
25129
25130 fs.set_head_for_repo(
25131 path!("/test/.git").as_ref(),
25132 &[
25133 ("file-1", "one\n".into()),
25134 ("file-2", "two\n".into()),
25135 ("file-3", "three\n".into()),
25136 ],
25137 "deadbeef",
25138 );
25139
25140 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
25141 let mut buffers = vec![];
25142 for i in 1..=3 {
25143 let buffer = project
25144 .update(cx, |project, cx| {
25145 let path = format!(path!("/test/file-{}"), i);
25146 project.open_local_buffer(path, cx)
25147 })
25148 .await
25149 .unwrap();
25150 buffers.push(buffer);
25151 }
25152
25153 let multibuffer = cx.new(|cx| {
25154 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
25155 multibuffer.set_all_diff_hunks_expanded(cx);
25156 for buffer in &buffers {
25157 let snapshot = buffer.read(cx).snapshot();
25158 multibuffer.set_excerpts_for_path(
25159 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
25160 buffer.clone(),
25161 vec![Point::zero()..snapshot.max_point()],
25162 2,
25163 cx,
25164 );
25165 }
25166 multibuffer
25167 });
25168
25169 let editor = cx.add_window(|window, cx| {
25170 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
25171 });
25172 cx.run_until_parked();
25173
25174 let snapshot = editor
25175 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25176 .unwrap();
25177 let hunks = snapshot
25178 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
25179 .map(|hunk| match hunk {
25180 DisplayDiffHunk::Unfolded {
25181 display_row_range, ..
25182 } => display_row_range,
25183 DisplayDiffHunk::Folded { .. } => unreachable!(),
25184 })
25185 .collect::<Vec<_>>();
25186 assert_eq!(
25187 hunks,
25188 [
25189 DisplayRow(2)..DisplayRow(4),
25190 DisplayRow(7)..DisplayRow(9),
25191 DisplayRow(12)..DisplayRow(14),
25192 ]
25193 );
25194}
25195
25196#[gpui::test]
25197async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
25198 init_test(cx, |_| {});
25199
25200 let mut cx = EditorTestContext::new(cx).await;
25201 cx.set_head_text(indoc! { "
25202 one
25203 two
25204 three
25205 four
25206 five
25207 "
25208 });
25209 cx.set_index_text(indoc! { "
25210 one
25211 two
25212 three
25213 four
25214 five
25215 "
25216 });
25217 cx.set_state(indoc! {"
25218 one
25219 TWO
25220 ˇTHREE
25221 FOUR
25222 five
25223 "});
25224 cx.run_until_parked();
25225 cx.update_editor(|editor, window, cx| {
25226 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
25227 });
25228 cx.run_until_parked();
25229 cx.assert_index_text(Some(indoc! {"
25230 one
25231 TWO
25232 THREE
25233 FOUR
25234 five
25235 "}));
25236 cx.set_state(indoc! { "
25237 one
25238 TWO
25239 ˇTHREE-HUNDRED
25240 FOUR
25241 five
25242 "});
25243 cx.run_until_parked();
25244 cx.update_editor(|editor, window, cx| {
25245 let snapshot = editor.snapshot(window, cx);
25246 let hunks = editor
25247 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
25248 .collect::<Vec<_>>();
25249 assert_eq!(hunks.len(), 1);
25250 assert_eq!(
25251 hunks[0].status(),
25252 DiffHunkStatus {
25253 kind: DiffHunkStatusKind::Modified,
25254 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
25255 }
25256 );
25257
25258 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
25259 });
25260 cx.run_until_parked();
25261 cx.assert_index_text(Some(indoc! {"
25262 one
25263 TWO
25264 THREE-HUNDRED
25265 FOUR
25266 five
25267 "}));
25268}
25269
25270#[gpui::test]
25271fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
25272 init_test(cx, |_| {});
25273
25274 let editor = cx.add_window(|window, cx| {
25275 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
25276 build_editor(buffer, window, cx)
25277 });
25278
25279 let render_args = Arc::new(Mutex::new(None));
25280 let snapshot = editor
25281 .update(cx, |editor, window, cx| {
25282 let snapshot = editor.buffer().read(cx).snapshot(cx);
25283 let range =
25284 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
25285
25286 struct RenderArgs {
25287 row: MultiBufferRow,
25288 folded: bool,
25289 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
25290 }
25291
25292 let crease = Crease::inline(
25293 range,
25294 FoldPlaceholder::test(),
25295 {
25296 let toggle_callback = render_args.clone();
25297 move |row, folded, callback, _window, _cx| {
25298 *toggle_callback.lock() = Some(RenderArgs {
25299 row,
25300 folded,
25301 callback,
25302 });
25303 div()
25304 }
25305 },
25306 |_row, _folded, _window, _cx| div(),
25307 );
25308
25309 editor.insert_creases(Some(crease), cx);
25310 let snapshot = editor.snapshot(window, cx);
25311 let _div =
25312 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
25313 snapshot
25314 })
25315 .unwrap();
25316
25317 let render_args = render_args.lock().take().unwrap();
25318 assert_eq!(render_args.row, MultiBufferRow(1));
25319 assert!(!render_args.folded);
25320 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
25321
25322 cx.update_window(*editor, |_, window, cx| {
25323 (render_args.callback)(true, window, cx)
25324 })
25325 .unwrap();
25326 let snapshot = editor
25327 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25328 .unwrap();
25329 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
25330
25331 cx.update_window(*editor, |_, window, cx| {
25332 (render_args.callback)(false, window, cx)
25333 })
25334 .unwrap();
25335 let snapshot = editor
25336 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25337 .unwrap();
25338 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
25339}
25340
25341#[gpui::test]
25342async fn test_input_text(cx: &mut TestAppContext) {
25343 init_test(cx, |_| {});
25344 let mut cx = EditorTestContext::new(cx).await;
25345
25346 cx.set_state(
25347 &r#"ˇone
25348 two
25349
25350 three
25351 fourˇ
25352 five
25353
25354 siˇx"#
25355 .unindent(),
25356 );
25357
25358 cx.dispatch_action(HandleInput(String::new()));
25359 cx.assert_editor_state(
25360 &r#"ˇone
25361 two
25362
25363 three
25364 fourˇ
25365 five
25366
25367 siˇx"#
25368 .unindent(),
25369 );
25370
25371 cx.dispatch_action(HandleInput("AAAA".to_string()));
25372 cx.assert_editor_state(
25373 &r#"AAAAˇone
25374 two
25375
25376 three
25377 fourAAAAˇ
25378 five
25379
25380 siAAAAˇx"#
25381 .unindent(),
25382 );
25383}
25384
25385#[gpui::test]
25386async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
25387 init_test(cx, |_| {});
25388
25389 let mut cx = EditorTestContext::new(cx).await;
25390 cx.set_state(
25391 r#"let foo = 1;
25392let foo = 2;
25393let foo = 3;
25394let fooˇ = 4;
25395let foo = 5;
25396let foo = 6;
25397let foo = 7;
25398let foo = 8;
25399let foo = 9;
25400let foo = 10;
25401let foo = 11;
25402let foo = 12;
25403let foo = 13;
25404let foo = 14;
25405let foo = 15;"#,
25406 );
25407
25408 cx.update_editor(|e, window, cx| {
25409 assert_eq!(
25410 e.next_scroll_position,
25411 NextScrollCursorCenterTopBottom::Center,
25412 "Default next scroll direction is center",
25413 );
25414
25415 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25416 assert_eq!(
25417 e.next_scroll_position,
25418 NextScrollCursorCenterTopBottom::Top,
25419 "After center, next scroll direction should be top",
25420 );
25421
25422 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25423 assert_eq!(
25424 e.next_scroll_position,
25425 NextScrollCursorCenterTopBottom::Bottom,
25426 "After top, next scroll direction should be bottom",
25427 );
25428
25429 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25430 assert_eq!(
25431 e.next_scroll_position,
25432 NextScrollCursorCenterTopBottom::Center,
25433 "After bottom, scrolling should start over",
25434 );
25435
25436 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25437 assert_eq!(
25438 e.next_scroll_position,
25439 NextScrollCursorCenterTopBottom::Top,
25440 "Scrolling continues if retriggered fast enough"
25441 );
25442 });
25443
25444 cx.executor()
25445 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
25446 cx.executor().run_until_parked();
25447 cx.update_editor(|e, _, _| {
25448 assert_eq!(
25449 e.next_scroll_position,
25450 NextScrollCursorCenterTopBottom::Center,
25451 "If scrolling is not triggered fast enough, it should reset"
25452 );
25453 });
25454}
25455
25456#[gpui::test]
25457async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
25458 init_test(cx, |_| {});
25459 let mut cx = EditorLspTestContext::new_rust(
25460 lsp::ServerCapabilities {
25461 definition_provider: Some(lsp::OneOf::Left(true)),
25462 references_provider: Some(lsp::OneOf::Left(true)),
25463 ..lsp::ServerCapabilities::default()
25464 },
25465 cx,
25466 )
25467 .await;
25468
25469 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
25470 let go_to_definition = cx
25471 .lsp
25472 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
25473 move |params, _| async move {
25474 if empty_go_to_definition {
25475 Ok(None)
25476 } else {
25477 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
25478 uri: params.text_document_position_params.text_document.uri,
25479 range: lsp::Range::new(
25480 lsp::Position::new(4, 3),
25481 lsp::Position::new(4, 6),
25482 ),
25483 })))
25484 }
25485 },
25486 );
25487 let references = cx
25488 .lsp
25489 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25490 Ok(Some(vec![lsp::Location {
25491 uri: params.text_document_position.text_document.uri,
25492 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
25493 }]))
25494 });
25495 (go_to_definition, references)
25496 };
25497
25498 cx.set_state(
25499 &r#"fn one() {
25500 let mut a = ˇtwo();
25501 }
25502
25503 fn two() {}"#
25504 .unindent(),
25505 );
25506 set_up_lsp_handlers(false, &mut cx);
25507 let navigated = cx
25508 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25509 .await
25510 .expect("Failed to navigate to definition");
25511 assert_eq!(
25512 navigated,
25513 Navigated::Yes,
25514 "Should have navigated to definition from the GetDefinition response"
25515 );
25516 cx.assert_editor_state(
25517 &r#"fn one() {
25518 let mut a = two();
25519 }
25520
25521 fn «twoˇ»() {}"#
25522 .unindent(),
25523 );
25524
25525 let editors = cx.update_workspace(|workspace, _, cx| {
25526 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25527 });
25528 cx.update_editor(|_, _, test_editor_cx| {
25529 assert_eq!(
25530 editors.len(),
25531 1,
25532 "Initially, only one, test, editor should be open in the workspace"
25533 );
25534 assert_eq!(
25535 test_editor_cx.entity(),
25536 editors.last().expect("Asserted len is 1").clone()
25537 );
25538 });
25539
25540 set_up_lsp_handlers(true, &mut cx);
25541 let navigated = cx
25542 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25543 .await
25544 .expect("Failed to navigate to lookup references");
25545 assert_eq!(
25546 navigated,
25547 Navigated::Yes,
25548 "Should have navigated to references as a fallback after empty GoToDefinition response"
25549 );
25550 // We should not change the selections in the existing file,
25551 // if opening another milti buffer with the references
25552 cx.assert_editor_state(
25553 &r#"fn one() {
25554 let mut a = two();
25555 }
25556
25557 fn «twoˇ»() {}"#
25558 .unindent(),
25559 );
25560 let editors = cx.update_workspace(|workspace, _, cx| {
25561 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25562 });
25563 cx.update_editor(|_, _, test_editor_cx| {
25564 assert_eq!(
25565 editors.len(),
25566 2,
25567 "After falling back to references search, we open a new editor with the results"
25568 );
25569 let references_fallback_text = editors
25570 .into_iter()
25571 .find(|new_editor| *new_editor != test_editor_cx.entity())
25572 .expect("Should have one non-test editor now")
25573 .read(test_editor_cx)
25574 .text(test_editor_cx);
25575 assert_eq!(
25576 references_fallback_text, "fn one() {\n let mut a = two();\n}",
25577 "Should use the range from the references response and not the GoToDefinition one"
25578 );
25579 });
25580}
25581
25582#[gpui::test]
25583async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
25584 init_test(cx, |_| {});
25585 cx.update(|cx| {
25586 let mut editor_settings = EditorSettings::get_global(cx).clone();
25587 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
25588 EditorSettings::override_global(editor_settings, cx);
25589 });
25590 let mut cx = EditorLspTestContext::new_rust(
25591 lsp::ServerCapabilities {
25592 definition_provider: Some(lsp::OneOf::Left(true)),
25593 references_provider: Some(lsp::OneOf::Left(true)),
25594 ..lsp::ServerCapabilities::default()
25595 },
25596 cx,
25597 )
25598 .await;
25599 let original_state = r#"fn one() {
25600 let mut a = ˇtwo();
25601 }
25602
25603 fn two() {}"#
25604 .unindent();
25605 cx.set_state(&original_state);
25606
25607 let mut go_to_definition = cx
25608 .lsp
25609 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
25610 move |_, _| async move { Ok(None) },
25611 );
25612 let _references = cx
25613 .lsp
25614 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
25615 panic!("Should not call for references with no go to definition fallback")
25616 });
25617
25618 let navigated = cx
25619 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25620 .await
25621 .expect("Failed to navigate to lookup references");
25622 go_to_definition
25623 .next()
25624 .await
25625 .expect("Should have called the go_to_definition handler");
25626
25627 assert_eq!(
25628 navigated,
25629 Navigated::No,
25630 "Should have navigated to references as a fallback after empty GoToDefinition response"
25631 );
25632 cx.assert_editor_state(&original_state);
25633 let editors = cx.update_workspace(|workspace, _, cx| {
25634 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25635 });
25636 cx.update_editor(|_, _, _| {
25637 assert_eq!(
25638 editors.len(),
25639 1,
25640 "After unsuccessful fallback, no other editor should have been opened"
25641 );
25642 });
25643}
25644
25645#[gpui::test]
25646async fn test_goto_definition_close_ranges_open_singleton(cx: &mut TestAppContext) {
25647 init_test(cx, |_| {});
25648 let mut cx = EditorLspTestContext::new_rust(
25649 lsp::ServerCapabilities {
25650 definition_provider: Some(lsp::OneOf::Left(true)),
25651 ..lsp::ServerCapabilities::default()
25652 },
25653 cx,
25654 )
25655 .await;
25656
25657 // File content: 10 lines with functions defined on lines 3, 5, and 7 (0-indexed).
25658 // With the default excerpt_context_lines of 2, ranges that are within
25659 // 2 * 2 = 4 rows of each other should be grouped into one excerpt.
25660 cx.set_state(
25661 &r#"fn caller() {
25662 let _ = ˇtarget();
25663 }
25664 fn target_a() {}
25665
25666 fn target_b() {}
25667
25668 fn target_c() {}
25669 "#
25670 .unindent(),
25671 );
25672
25673 // Return two definitions that are close together (lines 3 and 5, gap of 2 rows)
25674 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25675 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25676 lsp::Location {
25677 uri: url.clone(),
25678 range: lsp::Range::new(lsp::Position::new(3, 3), lsp::Position::new(3, 11)),
25679 },
25680 lsp::Location {
25681 uri: url,
25682 range: lsp::Range::new(lsp::Position::new(5, 3), lsp::Position::new(5, 11)),
25683 },
25684 ])))
25685 });
25686
25687 let navigated = cx
25688 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25689 .await
25690 .expect("Failed to navigate to definitions");
25691 assert_eq!(navigated, Navigated::Yes);
25692
25693 let editors = cx.update_workspace(|workspace, _, cx| {
25694 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25695 });
25696 cx.update_editor(|_, _, _| {
25697 assert_eq!(
25698 editors.len(),
25699 1,
25700 "Close ranges should navigate in-place without opening a new editor"
25701 );
25702 });
25703
25704 // Both target ranges should be selected
25705 cx.assert_editor_state(
25706 &r#"fn caller() {
25707 let _ = target();
25708 }
25709 fn «target_aˇ»() {}
25710
25711 fn «target_bˇ»() {}
25712
25713 fn target_c() {}
25714 "#
25715 .unindent(),
25716 );
25717}
25718
25719#[gpui::test]
25720async fn test_goto_definition_far_ranges_open_multibuffer(cx: &mut TestAppContext) {
25721 init_test(cx, |_| {});
25722 let mut cx = EditorLspTestContext::new_rust(
25723 lsp::ServerCapabilities {
25724 definition_provider: Some(lsp::OneOf::Left(true)),
25725 ..lsp::ServerCapabilities::default()
25726 },
25727 cx,
25728 )
25729 .await;
25730
25731 // Create a file with definitions far apart (more than 2 * excerpt_context_lines rows).
25732 cx.set_state(
25733 &r#"fn caller() {
25734 let _ = ˇtarget();
25735 }
25736 fn target_a() {}
25737
25738
25739
25740
25741
25742
25743
25744
25745
25746
25747
25748
25749
25750
25751
25752 fn target_b() {}
25753 "#
25754 .unindent(),
25755 );
25756
25757 // Return two definitions that are far apart (lines 3 and 19, gap of 16 rows)
25758 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25759 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25760 lsp::Location {
25761 uri: url.clone(),
25762 range: lsp::Range::new(lsp::Position::new(3, 3), lsp::Position::new(3, 11)),
25763 },
25764 lsp::Location {
25765 uri: url,
25766 range: lsp::Range::new(lsp::Position::new(19, 3), lsp::Position::new(19, 11)),
25767 },
25768 ])))
25769 });
25770
25771 let navigated = cx
25772 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25773 .await
25774 .expect("Failed to navigate to definitions");
25775 assert_eq!(navigated, Navigated::Yes);
25776
25777 let editors = cx.update_workspace(|workspace, _, cx| {
25778 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25779 });
25780 cx.update_editor(|_, _, test_editor_cx| {
25781 assert_eq!(
25782 editors.len(),
25783 2,
25784 "Far apart ranges should open a new multibuffer editor"
25785 );
25786 let multibuffer_editor = editors
25787 .into_iter()
25788 .find(|editor| *editor != test_editor_cx.entity())
25789 .expect("Should have a multibuffer editor");
25790 let multibuffer_text = multibuffer_editor.read(test_editor_cx).text(test_editor_cx);
25791 assert!(
25792 multibuffer_text.contains("target_a"),
25793 "Multibuffer should contain the first definition"
25794 );
25795 assert!(
25796 multibuffer_text.contains("target_b"),
25797 "Multibuffer should contain the second definition"
25798 );
25799 });
25800}
25801
25802#[gpui::test]
25803async fn test_goto_definition_contained_ranges(cx: &mut TestAppContext) {
25804 init_test(cx, |_| {});
25805 let mut cx = EditorLspTestContext::new_rust(
25806 lsp::ServerCapabilities {
25807 definition_provider: Some(lsp::OneOf::Left(true)),
25808 ..lsp::ServerCapabilities::default()
25809 },
25810 cx,
25811 )
25812 .await;
25813
25814 // The LSP returns two single-line definitions on the same row where one
25815 // range contains the other. Both are on the same line so the
25816 // `fits_in_one_excerpt` check won't underflow, and the code reaches
25817 // `change_selections`.
25818 cx.set_state(
25819 &r#"fn caller() {
25820 let _ = ˇtarget();
25821 }
25822 fn target_outer() { fn target_inner() {} }
25823 "#
25824 .unindent(),
25825 );
25826
25827 // Return two definitions on the same line: an outer range covering the
25828 // whole line and an inner range for just the inner function name.
25829 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25830 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25831 // Inner range: just "target_inner" (cols 23..35)
25832 lsp::Location {
25833 uri: url.clone(),
25834 range: lsp::Range::new(lsp::Position::new(3, 23), lsp::Position::new(3, 35)),
25835 },
25836 // Outer range: the whole line (cols 0..48)
25837 lsp::Location {
25838 uri: url,
25839 range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 48)),
25840 },
25841 ])))
25842 });
25843
25844 let navigated = cx
25845 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25846 .await
25847 .expect("Failed to navigate to definitions");
25848 assert_eq!(navigated, Navigated::Yes);
25849}
25850
25851#[gpui::test]
25852async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
25853 init_test(cx, |_| {});
25854 let mut cx = EditorLspTestContext::new_rust(
25855 lsp::ServerCapabilities {
25856 references_provider: Some(lsp::OneOf::Left(true)),
25857 ..lsp::ServerCapabilities::default()
25858 },
25859 cx,
25860 )
25861 .await;
25862
25863 cx.set_state(
25864 &r#"
25865 fn one() {
25866 let mut a = two();
25867 }
25868
25869 fn ˇtwo() {}"#
25870 .unindent(),
25871 );
25872 cx.lsp
25873 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25874 Ok(Some(vec![
25875 lsp::Location {
25876 uri: params.text_document_position.text_document.uri.clone(),
25877 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
25878 },
25879 lsp::Location {
25880 uri: params.text_document_position.text_document.uri,
25881 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
25882 },
25883 ]))
25884 });
25885 let navigated = cx
25886 .update_editor(|editor, window, cx| {
25887 editor.find_all_references(&FindAllReferences::default(), window, cx)
25888 })
25889 .unwrap()
25890 .await
25891 .expect("Failed to navigate to references");
25892 assert_eq!(
25893 navigated,
25894 Navigated::Yes,
25895 "Should have navigated to references from the FindAllReferences response"
25896 );
25897 cx.assert_editor_state(
25898 &r#"fn one() {
25899 let mut a = two();
25900 }
25901
25902 fn ˇtwo() {}"#
25903 .unindent(),
25904 );
25905
25906 let editors = cx.update_workspace(|workspace, _, cx| {
25907 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25908 });
25909 cx.update_editor(|_, _, _| {
25910 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
25911 });
25912
25913 cx.set_state(
25914 &r#"fn one() {
25915 let mut a = ˇtwo();
25916 }
25917
25918 fn two() {}"#
25919 .unindent(),
25920 );
25921 let navigated = cx
25922 .update_editor(|editor, window, cx| {
25923 editor.find_all_references(&FindAllReferences::default(), window, cx)
25924 })
25925 .unwrap()
25926 .await
25927 .expect("Failed to navigate to references");
25928 assert_eq!(
25929 navigated,
25930 Navigated::Yes,
25931 "Should have navigated to references from the FindAllReferences response"
25932 );
25933 cx.assert_editor_state(
25934 &r#"fn one() {
25935 let mut a = ˇtwo();
25936 }
25937
25938 fn two() {}"#
25939 .unindent(),
25940 );
25941 let editors = cx.update_workspace(|workspace, _, cx| {
25942 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25943 });
25944 cx.update_editor(|_, _, _| {
25945 assert_eq!(
25946 editors.len(),
25947 2,
25948 "should have re-used the previous multibuffer"
25949 );
25950 });
25951
25952 cx.set_state(
25953 &r#"fn one() {
25954 let mut a = ˇtwo();
25955 }
25956 fn three() {}
25957 fn two() {}"#
25958 .unindent(),
25959 );
25960 cx.lsp
25961 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25962 Ok(Some(vec![
25963 lsp::Location {
25964 uri: params.text_document_position.text_document.uri.clone(),
25965 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
25966 },
25967 lsp::Location {
25968 uri: params.text_document_position.text_document.uri,
25969 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
25970 },
25971 ]))
25972 });
25973 let navigated = cx
25974 .update_editor(|editor, window, cx| {
25975 editor.find_all_references(&FindAllReferences::default(), window, cx)
25976 })
25977 .unwrap()
25978 .await
25979 .expect("Failed to navigate to references");
25980 assert_eq!(
25981 navigated,
25982 Navigated::Yes,
25983 "Should have navigated to references from the FindAllReferences response"
25984 );
25985 cx.assert_editor_state(
25986 &r#"fn one() {
25987 let mut a = ˇtwo();
25988 }
25989 fn three() {}
25990 fn two() {}"#
25991 .unindent(),
25992 );
25993 let editors = cx.update_workspace(|workspace, _, cx| {
25994 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25995 });
25996 cx.update_editor(|_, _, _| {
25997 assert_eq!(
25998 editors.len(),
25999 3,
26000 "should have used a new multibuffer as offsets changed"
26001 );
26002 });
26003}
26004#[gpui::test]
26005async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
26006 init_test(cx, |_| {});
26007
26008 let language = Arc::new(Language::new(
26009 LanguageConfig::default(),
26010 Some(tree_sitter_rust::LANGUAGE.into()),
26011 ));
26012
26013 let text = r#"
26014 #[cfg(test)]
26015 mod tests() {
26016 #[test]
26017 fn runnable_1() {
26018 let a = 1;
26019 }
26020
26021 #[test]
26022 fn runnable_2() {
26023 let a = 1;
26024 let b = 2;
26025 }
26026 }
26027 "#
26028 .unindent();
26029
26030 let fs = FakeFs::new(cx.executor());
26031 fs.insert_file("/file.rs", Default::default()).await;
26032
26033 let project = Project::test(fs, ["/a".as_ref()], cx).await;
26034 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26035 let cx = &mut VisualTestContext::from_window(*window, cx);
26036 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26037 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
26038
26039 let editor = cx.new_window_entity(|window, cx| {
26040 Editor::new(
26041 EditorMode::full(),
26042 multi_buffer,
26043 Some(project.clone()),
26044 window,
26045 cx,
26046 )
26047 });
26048
26049 editor.update_in(cx, |editor, window, cx| {
26050 let snapshot = editor.buffer().read(cx).snapshot(cx);
26051 editor.runnables.insert(
26052 buffer.read(cx).remote_id(),
26053 3,
26054 buffer.read(cx).version(),
26055 RunnableTasks {
26056 templates: Vec::new(),
26057 offset: snapshot.anchor_before(MultiBufferOffset(43)),
26058 column: 0,
26059 extra_variables: HashMap::default(),
26060 context_range: BufferOffset(43)..BufferOffset(85),
26061 },
26062 );
26063 editor.runnables.insert(
26064 buffer.read(cx).remote_id(),
26065 8,
26066 buffer.read(cx).version(),
26067 RunnableTasks {
26068 templates: Vec::new(),
26069 offset: snapshot.anchor_before(MultiBufferOffset(86)),
26070 column: 0,
26071 extra_variables: HashMap::default(),
26072 context_range: BufferOffset(86)..BufferOffset(191),
26073 },
26074 );
26075
26076 // Test finding task when cursor is inside function body
26077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26078 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
26079 });
26080 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
26081 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
26082
26083 // Test finding task when cursor is on function name
26084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26085 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
26086 });
26087 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
26088 assert_eq!(row, 8, "Should find task when cursor is on function name");
26089 });
26090}
26091
26092#[gpui::test]
26093async fn test_folding_buffers(cx: &mut TestAppContext) {
26094 init_test(cx, |_| {});
26095
26096 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
26097 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
26098 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
26099
26100 let fs = FakeFs::new(cx.executor());
26101 fs.insert_tree(
26102 path!("/a"),
26103 json!({
26104 "first.rs": sample_text_1,
26105 "second.rs": sample_text_2,
26106 "third.rs": sample_text_3,
26107 }),
26108 )
26109 .await;
26110 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26111 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26112 let cx = &mut VisualTestContext::from_window(*window, cx);
26113 let worktree = project.update(cx, |project, cx| {
26114 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26115 assert_eq!(worktrees.len(), 1);
26116 worktrees.pop().unwrap()
26117 });
26118 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26119
26120 let buffer_1 = project
26121 .update(cx, |project, cx| {
26122 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
26123 })
26124 .await
26125 .unwrap();
26126 let buffer_2 = project
26127 .update(cx, |project, cx| {
26128 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
26129 })
26130 .await
26131 .unwrap();
26132 let buffer_3 = project
26133 .update(cx, |project, cx| {
26134 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
26135 })
26136 .await
26137 .unwrap();
26138
26139 let multi_buffer = cx.new(|cx| {
26140 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26141 multi_buffer.set_excerpts_for_path(
26142 PathKey::sorted(0),
26143 buffer_1.clone(),
26144 [
26145 Point::new(0, 0)..Point::new(2, 0),
26146 Point::new(5, 0)..Point::new(6, 0),
26147 Point::new(9, 0)..Point::new(10, 4),
26148 ],
26149 0,
26150 cx,
26151 );
26152 multi_buffer.set_excerpts_for_path(
26153 PathKey::sorted(1),
26154 buffer_2.clone(),
26155 [
26156 Point::new(0, 0)..Point::new(2, 0),
26157 Point::new(5, 0)..Point::new(6, 0),
26158 Point::new(9, 0)..Point::new(10, 4),
26159 ],
26160 0,
26161 cx,
26162 );
26163 multi_buffer.set_excerpts_for_path(
26164 PathKey::sorted(2),
26165 buffer_3.clone(),
26166 [
26167 Point::new(0, 0)..Point::new(2, 0),
26168 Point::new(5, 0)..Point::new(6, 0),
26169 Point::new(9, 0)..Point::new(10, 4),
26170 ],
26171 0,
26172 cx,
26173 );
26174 multi_buffer
26175 });
26176 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26177 Editor::new(
26178 EditorMode::full(),
26179 multi_buffer.clone(),
26180 Some(project.clone()),
26181 window,
26182 cx,
26183 )
26184 });
26185
26186 assert_eq!(
26187 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26188 "\n\naaaa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26189 );
26190
26191 multi_buffer_editor.update(cx, |editor, cx| {
26192 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
26193 });
26194 assert_eq!(
26195 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26196 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26197 "After folding the first buffer, its text should not be displayed"
26198 );
26199
26200 multi_buffer_editor.update(cx, |editor, cx| {
26201 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
26202 });
26203 assert_eq!(
26204 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26205 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26206 "After folding the second buffer, its text should not be displayed"
26207 );
26208
26209 multi_buffer_editor.update(cx, |editor, cx| {
26210 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
26211 });
26212 assert_eq!(
26213 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26214 "\n\n\n\n\n",
26215 "After folding the third buffer, its text should not be displayed"
26216 );
26217
26218 // Emulate selection inside the fold logic, that should work
26219 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26220 editor
26221 .snapshot(window, cx)
26222 .next_line_boundary(Point::new(0, 4));
26223 });
26224
26225 multi_buffer_editor.update(cx, |editor, cx| {
26226 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
26227 });
26228 assert_eq!(
26229 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26230 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
26231 "After unfolding the second buffer, its text should be displayed"
26232 );
26233
26234 // Typing inside of buffer 1 causes that buffer to be unfolded.
26235 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26236 assert_eq!(
26237 multi_buffer
26238 .read(cx)
26239 .snapshot(cx)
26240 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
26241 .collect::<String>(),
26242 "bbbb"
26243 );
26244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
26245 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
26246 });
26247 editor.handle_input("B", window, cx);
26248 });
26249
26250 assert_eq!(
26251 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26252 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
26253 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
26254 );
26255
26256 multi_buffer_editor.update(cx, |editor, cx| {
26257 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
26258 });
26259 assert_eq!(
26260 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26261 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26262 "After unfolding the all buffers, all original text should be displayed"
26263 );
26264}
26265
26266#[gpui::test]
26267async fn test_folded_buffers_cleared_on_excerpts_removed(cx: &mut TestAppContext) {
26268 init_test(cx, |_| {});
26269
26270 let fs = FakeFs::new(cx.executor());
26271 fs.insert_tree(
26272 path!("/root"),
26273 json!({
26274 "file_a.txt": "File A\nFile A\nFile A",
26275 "file_b.txt": "File B\nFile B\nFile B",
26276 }),
26277 )
26278 .await;
26279
26280 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
26281 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26282 let cx = &mut VisualTestContext::from_window(*window, cx);
26283 let worktree = project.update(cx, |project, cx| {
26284 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26285 assert_eq!(worktrees.len(), 1);
26286 worktrees.pop().unwrap()
26287 });
26288 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26289
26290 let buffer_a = project
26291 .update(cx, |project, cx| {
26292 project.open_buffer((worktree_id, rel_path("file_a.txt")), cx)
26293 })
26294 .await
26295 .unwrap();
26296 let buffer_b = project
26297 .update(cx, |project, cx| {
26298 project.open_buffer((worktree_id, rel_path("file_b.txt")), cx)
26299 })
26300 .await
26301 .unwrap();
26302
26303 let multi_buffer = cx.new(|cx| {
26304 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26305 let range_a = Point::new(0, 0)..Point::new(2, 4);
26306 let range_b = Point::new(0, 0)..Point::new(2, 4);
26307
26308 multi_buffer.set_excerpts_for_path(PathKey::sorted(0), buffer_a.clone(), [range_a], 0, cx);
26309 multi_buffer.set_excerpts_for_path(PathKey::sorted(1), buffer_b.clone(), [range_b], 0, cx);
26310 multi_buffer
26311 });
26312
26313 let editor = cx.new_window_entity(|window, cx| {
26314 Editor::new(
26315 EditorMode::full(),
26316 multi_buffer.clone(),
26317 Some(project.clone()),
26318 window,
26319 cx,
26320 )
26321 });
26322
26323 editor.update(cx, |editor, cx| {
26324 editor.fold_buffer(buffer_a.read(cx).remote_id(), cx);
26325 });
26326 assert!(editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
26327
26328 // When the excerpts for `buffer_a` are removed, a
26329 // `multi_buffer::Event::ExcerptsRemoved` event is emitted, which should be
26330 // picked up by the editor and update the display map accordingly.
26331 multi_buffer.update(cx, |multi_buffer, cx| {
26332 multi_buffer.remove_excerpts(PathKey::sorted(0), cx)
26333 });
26334 assert!(!editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
26335}
26336
26337#[gpui::test]
26338async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
26339 init_test(cx, |_| {});
26340
26341 let sample_text_1 = "1111\n2222\n3333".to_string();
26342 let sample_text_2 = "4444\n5555\n6666".to_string();
26343 let sample_text_3 = "7777\n8888\n9999".to_string();
26344
26345 let fs = FakeFs::new(cx.executor());
26346 fs.insert_tree(
26347 path!("/a"),
26348 json!({
26349 "first.rs": sample_text_1,
26350 "second.rs": sample_text_2,
26351 "third.rs": sample_text_3,
26352 }),
26353 )
26354 .await;
26355 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26356 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26357 let cx = &mut VisualTestContext::from_window(*window, cx);
26358 let worktree = project.update(cx, |project, cx| {
26359 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26360 assert_eq!(worktrees.len(), 1);
26361 worktrees.pop().unwrap()
26362 });
26363 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26364
26365 let buffer_1 = project
26366 .update(cx, |project, cx| {
26367 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
26368 })
26369 .await
26370 .unwrap();
26371 let buffer_2 = project
26372 .update(cx, |project, cx| {
26373 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
26374 })
26375 .await
26376 .unwrap();
26377 let buffer_3 = project
26378 .update(cx, |project, cx| {
26379 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
26380 })
26381 .await
26382 .unwrap();
26383
26384 let multi_buffer = cx.new(|cx| {
26385 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26386 multi_buffer.set_excerpts_for_path(
26387 PathKey::sorted(0),
26388 buffer_1.clone(),
26389 [Point::new(0, 0)..Point::new(3, 0)],
26390 0,
26391 cx,
26392 );
26393 multi_buffer.set_excerpts_for_path(
26394 PathKey::sorted(1),
26395 buffer_2.clone(),
26396 [Point::new(0, 0)..Point::new(3, 0)],
26397 0,
26398 cx,
26399 );
26400 multi_buffer.set_excerpts_for_path(
26401 PathKey::sorted(2),
26402 buffer_3.clone(),
26403 [Point::new(0, 0)..Point::new(3, 0)],
26404 0,
26405 cx,
26406 );
26407 multi_buffer
26408 });
26409
26410 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26411 Editor::new(
26412 EditorMode::full(),
26413 multi_buffer,
26414 Some(project.clone()),
26415 window,
26416 cx,
26417 )
26418 });
26419
26420 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
26421 assert_eq!(
26422 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26423 full_text,
26424 );
26425
26426 multi_buffer_editor.update(cx, |editor, cx| {
26427 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
26428 });
26429 assert_eq!(
26430 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26431 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
26432 "After folding the first buffer, its text should not be displayed"
26433 );
26434
26435 multi_buffer_editor.update(cx, |editor, cx| {
26436 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
26437 });
26438
26439 assert_eq!(
26440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26441 "\n\n\n\n\n\n7777\n8888\n9999",
26442 "After folding the second buffer, its text should not be displayed"
26443 );
26444
26445 multi_buffer_editor.update(cx, |editor, cx| {
26446 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
26447 });
26448 assert_eq!(
26449 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26450 "\n\n\n\n\n",
26451 "After folding the third buffer, its text should not be displayed"
26452 );
26453
26454 multi_buffer_editor.update(cx, |editor, cx| {
26455 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
26456 });
26457 assert_eq!(
26458 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26459 "\n\n\n\n4444\n5555\n6666\n\n",
26460 "After unfolding the second buffer, its text should be displayed"
26461 );
26462
26463 multi_buffer_editor.update(cx, |editor, cx| {
26464 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
26465 });
26466 assert_eq!(
26467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26468 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
26469 "After unfolding the first buffer, its text should be displayed"
26470 );
26471
26472 multi_buffer_editor.update(cx, |editor, cx| {
26473 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
26474 });
26475 assert_eq!(
26476 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26477 full_text,
26478 "After unfolding all buffers, all original text should be displayed"
26479 );
26480}
26481
26482#[gpui::test]
26483async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
26484 init_test(cx, |_| {});
26485
26486 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
26487
26488 let fs = FakeFs::new(cx.executor());
26489 fs.insert_tree(
26490 path!("/a"),
26491 json!({
26492 "main.rs": sample_text,
26493 }),
26494 )
26495 .await;
26496 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26497 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26498 let cx = &mut VisualTestContext::from_window(*window, cx);
26499 let worktree = project.update(cx, |project, cx| {
26500 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26501 assert_eq!(worktrees.len(), 1);
26502 worktrees.pop().unwrap()
26503 });
26504 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26505
26506 let buffer_1 = project
26507 .update(cx, |project, cx| {
26508 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
26509 })
26510 .await
26511 .unwrap();
26512
26513 let multi_buffer = cx.new(|cx| {
26514 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26515 multi_buffer.set_excerpts_for_path(
26516 PathKey::sorted(0),
26517 buffer_1.clone(),
26518 [Point::new(0, 0)
26519 ..Point::new(
26520 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
26521 0,
26522 )],
26523 0,
26524 cx,
26525 );
26526 multi_buffer
26527 });
26528 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26529 Editor::new(
26530 EditorMode::full(),
26531 multi_buffer,
26532 Some(project.clone()),
26533 window,
26534 cx,
26535 )
26536 });
26537
26538 let selection_range = Point::new(1, 0)..Point::new(2, 0);
26539 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26540 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26541 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
26542 editor.highlight_text(
26543 HighlightKey::Editor,
26544 vec![highlight_range.clone()],
26545 HighlightStyle::color(Hsla::green()),
26546 cx,
26547 );
26548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26549 s.select_ranges(Some(highlight_range))
26550 });
26551 });
26552
26553 let full_text = format!("\n\n{sample_text}");
26554 assert_eq!(
26555 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26556 full_text,
26557 );
26558}
26559
26560#[gpui::test]
26561async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
26562 init_test(cx, |_| {});
26563 cx.update(|cx| {
26564 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
26565 "keymaps/default-linux.json",
26566 cx,
26567 )
26568 .unwrap();
26569 cx.bind_keys(default_key_bindings);
26570 });
26571
26572 let (editor, cx) = cx.add_window_view(|window, cx| {
26573 let multi_buffer = MultiBuffer::build_multi(
26574 [
26575 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
26576 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
26577 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
26578 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
26579 ],
26580 cx,
26581 );
26582 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26583
26584 let buffer_ids = multi_buffer
26585 .read(cx)
26586 .snapshot(cx)
26587 .excerpts()
26588 .map(|excerpt| excerpt.context.start.buffer_id)
26589 .collect::<Vec<_>>();
26590 // fold all but the second buffer, so that we test navigating between two
26591 // adjacent folded buffers, as well as folded buffers at the start and
26592 // end the multibuffer
26593 editor.fold_buffer(buffer_ids[0], cx);
26594 editor.fold_buffer(buffer_ids[2], cx);
26595 editor.fold_buffer(buffer_ids[3], cx);
26596
26597 editor
26598 });
26599 cx.simulate_resize(size(px(1000.), px(1000.)));
26600
26601 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26602 cx.assert_excerpts_with_selections(indoc! {"
26603 [EXCERPT]
26604 ˇ[FOLDED]
26605 [EXCERPT]
26606 a1
26607 b1
26608 [EXCERPT]
26609 [FOLDED]
26610 [EXCERPT]
26611 [FOLDED]
26612 "
26613 });
26614 cx.simulate_keystroke("down");
26615 cx.assert_excerpts_with_selections(indoc! {"
26616 [EXCERPT]
26617 [FOLDED]
26618 [EXCERPT]
26619 ˇa1
26620 b1
26621 [EXCERPT]
26622 [FOLDED]
26623 [EXCERPT]
26624 [FOLDED]
26625 "
26626 });
26627 cx.simulate_keystroke("down");
26628 cx.assert_excerpts_with_selections(indoc! {"
26629 [EXCERPT]
26630 [FOLDED]
26631 [EXCERPT]
26632 a1
26633 ˇb1
26634 [EXCERPT]
26635 [FOLDED]
26636 [EXCERPT]
26637 [FOLDED]
26638 "
26639 });
26640 cx.simulate_keystroke("down");
26641 cx.assert_excerpts_with_selections(indoc! {"
26642 [EXCERPT]
26643 [FOLDED]
26644 [EXCERPT]
26645 a1
26646 b1
26647 ˇ[EXCERPT]
26648 [FOLDED]
26649 [EXCERPT]
26650 [FOLDED]
26651 "
26652 });
26653 cx.simulate_keystroke("down");
26654 cx.assert_excerpts_with_selections(indoc! {"
26655 [EXCERPT]
26656 [FOLDED]
26657 [EXCERPT]
26658 a1
26659 b1
26660 [EXCERPT]
26661 ˇ[FOLDED]
26662 [EXCERPT]
26663 [FOLDED]
26664 "
26665 });
26666 for _ in 0..5 {
26667 cx.simulate_keystroke("down");
26668 cx.assert_excerpts_with_selections(indoc! {"
26669 [EXCERPT]
26670 [FOLDED]
26671 [EXCERPT]
26672 a1
26673 b1
26674 [EXCERPT]
26675 [FOLDED]
26676 [EXCERPT]
26677 ˇ[FOLDED]
26678 "
26679 });
26680 }
26681
26682 cx.simulate_keystroke("up");
26683 cx.assert_excerpts_with_selections(indoc! {"
26684 [EXCERPT]
26685 [FOLDED]
26686 [EXCERPT]
26687 a1
26688 b1
26689 [EXCERPT]
26690 ˇ[FOLDED]
26691 [EXCERPT]
26692 [FOLDED]
26693 "
26694 });
26695 cx.simulate_keystroke("up");
26696 cx.assert_excerpts_with_selections(indoc! {"
26697 [EXCERPT]
26698 [FOLDED]
26699 [EXCERPT]
26700 a1
26701 b1
26702 ˇ[EXCERPT]
26703 [FOLDED]
26704 [EXCERPT]
26705 [FOLDED]
26706 "
26707 });
26708 cx.simulate_keystroke("up");
26709 cx.assert_excerpts_with_selections(indoc! {"
26710 [EXCERPT]
26711 [FOLDED]
26712 [EXCERPT]
26713 a1
26714 ˇb1
26715 [EXCERPT]
26716 [FOLDED]
26717 [EXCERPT]
26718 [FOLDED]
26719 "
26720 });
26721 cx.simulate_keystroke("up");
26722 cx.assert_excerpts_with_selections(indoc! {"
26723 [EXCERPT]
26724 [FOLDED]
26725 [EXCERPT]
26726 ˇa1
26727 b1
26728 [EXCERPT]
26729 [FOLDED]
26730 [EXCERPT]
26731 [FOLDED]
26732 "
26733 });
26734 for _ in 0..5 {
26735 cx.simulate_keystroke("up");
26736 cx.assert_excerpts_with_selections(indoc! {"
26737 [EXCERPT]
26738 ˇ[FOLDED]
26739 [EXCERPT]
26740 a1
26741 b1
26742 [EXCERPT]
26743 [FOLDED]
26744 [EXCERPT]
26745 [FOLDED]
26746 "
26747 });
26748 }
26749}
26750
26751#[gpui::test]
26752async fn test_edit_prediction_text(cx: &mut TestAppContext) {
26753 init_test(cx, |_| {});
26754
26755 // Simple insertion
26756 assert_highlighted_edits(
26757 "Hello, world!",
26758 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
26759 true,
26760 cx,
26761 &|highlighted_edits, cx| {
26762 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
26763 assert_eq!(highlighted_edits.highlights.len(), 1);
26764 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
26765 assert_eq!(
26766 highlighted_edits.highlights[0].1.background_color,
26767 Some(cx.theme().status().created_background)
26768 );
26769 },
26770 )
26771 .await;
26772
26773 // Replacement
26774 assert_highlighted_edits(
26775 "This is a test.",
26776 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
26777 false,
26778 cx,
26779 &|highlighted_edits, cx| {
26780 assert_eq!(highlighted_edits.text, "That is a test.");
26781 assert_eq!(highlighted_edits.highlights.len(), 1);
26782 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
26783 assert_eq!(
26784 highlighted_edits.highlights[0].1.background_color,
26785 Some(cx.theme().status().created_background)
26786 );
26787 },
26788 )
26789 .await;
26790
26791 // Multiple edits
26792 assert_highlighted_edits(
26793 "Hello, world!",
26794 vec![
26795 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
26796 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
26797 ],
26798 false,
26799 cx,
26800 &|highlighted_edits, cx| {
26801 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
26802 assert_eq!(highlighted_edits.highlights.len(), 2);
26803 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
26804 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
26805 assert_eq!(
26806 highlighted_edits.highlights[0].1.background_color,
26807 Some(cx.theme().status().created_background)
26808 );
26809 assert_eq!(
26810 highlighted_edits.highlights[1].1.background_color,
26811 Some(cx.theme().status().created_background)
26812 );
26813 },
26814 )
26815 .await;
26816
26817 // Multiple lines with edits
26818 assert_highlighted_edits(
26819 "First line\nSecond line\nThird line\nFourth line",
26820 vec![
26821 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
26822 (
26823 Point::new(2, 0)..Point::new(2, 10),
26824 "New third line".to_string(),
26825 ),
26826 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
26827 ],
26828 false,
26829 cx,
26830 &|highlighted_edits, cx| {
26831 assert_eq!(
26832 highlighted_edits.text,
26833 "Second modified\nNew third line\nFourth updated line"
26834 );
26835 assert_eq!(highlighted_edits.highlights.len(), 3);
26836 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
26837 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
26838 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
26839 for highlight in &highlighted_edits.highlights {
26840 assert_eq!(
26841 highlight.1.background_color,
26842 Some(cx.theme().status().created_background)
26843 );
26844 }
26845 },
26846 )
26847 .await;
26848}
26849
26850#[gpui::test]
26851async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
26852 init_test(cx, |_| {});
26853
26854 // Deletion
26855 assert_highlighted_edits(
26856 "Hello, world!",
26857 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
26858 true,
26859 cx,
26860 &|highlighted_edits, cx| {
26861 assert_eq!(highlighted_edits.text, "Hello, world!");
26862 assert_eq!(highlighted_edits.highlights.len(), 1);
26863 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
26864 assert_eq!(
26865 highlighted_edits.highlights[0].1.background_color,
26866 Some(cx.theme().status().deleted_background)
26867 );
26868 },
26869 )
26870 .await;
26871
26872 // Insertion
26873 assert_highlighted_edits(
26874 "Hello, world!",
26875 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
26876 true,
26877 cx,
26878 &|highlighted_edits, cx| {
26879 assert_eq!(highlighted_edits.highlights.len(), 1);
26880 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
26881 assert_eq!(
26882 highlighted_edits.highlights[0].1.background_color,
26883 Some(cx.theme().status().created_background)
26884 );
26885 },
26886 )
26887 .await;
26888}
26889
26890async fn assert_highlighted_edits(
26891 text: &str,
26892 edits: Vec<(Range<Point>, String)>,
26893 include_deletions: bool,
26894 cx: &mut TestAppContext,
26895 assertion_fn: &dyn Fn(HighlightedText, &App),
26896) {
26897 let window = cx.add_window(|window, cx| {
26898 let buffer = MultiBuffer::build_simple(text, cx);
26899 Editor::new(EditorMode::full(), buffer, None, window, cx)
26900 });
26901 let cx = &mut VisualTestContext::from_window(*window, cx);
26902
26903 let (buffer, snapshot) = window
26904 .update(cx, |editor, _window, cx| {
26905 (
26906 editor.buffer().clone(),
26907 editor.buffer().read(cx).snapshot(cx),
26908 )
26909 })
26910 .unwrap();
26911
26912 let edits = edits
26913 .into_iter()
26914 .map(|(range, edit)| {
26915 (
26916 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
26917 edit,
26918 )
26919 })
26920 .collect::<Vec<_>>();
26921
26922 let text_anchor_edits = edits
26923 .clone()
26924 .into_iter()
26925 .map(|(range, edit)| {
26926 (
26927 range.start.expect_text_anchor()..range.end.expect_text_anchor(),
26928 edit.into(),
26929 )
26930 })
26931 .collect::<Vec<_>>();
26932
26933 let edit_preview = window
26934 .update(cx, |_, _window, cx| {
26935 buffer
26936 .read(cx)
26937 .as_singleton()
26938 .unwrap()
26939 .read(cx)
26940 .preview_edits(text_anchor_edits.into(), cx)
26941 })
26942 .unwrap()
26943 .await;
26944
26945 cx.update(|_window, cx| {
26946 let highlighted_edits = edit_prediction_edit_text(
26947 snapshot.as_singleton().unwrap(),
26948 &edits,
26949 &edit_preview,
26950 include_deletions,
26951 &snapshot,
26952 cx,
26953 );
26954 assertion_fn(highlighted_edits, cx)
26955 });
26956}
26957
26958#[track_caller]
26959fn assert_breakpoint(
26960 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
26961 path: &Arc<Path>,
26962 expected: Vec<(u32, Breakpoint)>,
26963) {
26964 if expected.is_empty() {
26965 assert!(!breakpoints.contains_key(path), "{}", path.display());
26966 } else {
26967 let mut breakpoint = breakpoints
26968 .get(path)
26969 .unwrap()
26970 .iter()
26971 .map(|breakpoint| {
26972 (
26973 breakpoint.row,
26974 Breakpoint {
26975 message: breakpoint.message.clone(),
26976 state: breakpoint.state,
26977 condition: breakpoint.condition.clone(),
26978 hit_condition: breakpoint.hit_condition.clone(),
26979 },
26980 )
26981 })
26982 .collect::<Vec<_>>();
26983
26984 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
26985
26986 assert_eq!(expected, breakpoint);
26987 }
26988}
26989
26990fn add_log_breakpoint_at_cursor(
26991 editor: &mut Editor,
26992 log_message: &str,
26993 window: &mut Window,
26994 cx: &mut Context<Editor>,
26995) {
26996 let (anchor, bp) = editor
26997 .breakpoints_at_cursors(window, cx)
26998 .first()
26999 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
27000 .unwrap_or_else(|| {
27001 let snapshot = editor.snapshot(window, cx);
27002 let cursor_position: Point =
27003 editor.selections.newest(&snapshot.display_snapshot).head();
27004
27005 let breakpoint_position = snapshot
27006 .buffer_snapshot()
27007 .anchor_before(Point::new(cursor_position.row, 0));
27008
27009 (breakpoint_position, Breakpoint::new_log(log_message))
27010 });
27011
27012 editor.edit_breakpoint_at_anchor(
27013 anchor,
27014 bp,
27015 BreakpointEditAction::EditLogMessage(log_message.into()),
27016 cx,
27017 );
27018}
27019
27020#[gpui::test]
27021async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
27022 init_test(cx, |_| {});
27023
27024 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27025 let fs = FakeFs::new(cx.executor());
27026 fs.insert_tree(
27027 path!("/a"),
27028 json!({
27029 "main.rs": sample_text,
27030 }),
27031 )
27032 .await;
27033 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27034 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27035 let cx = &mut VisualTestContext::from_window(*window, cx);
27036
27037 let fs = FakeFs::new(cx.executor());
27038 fs.insert_tree(
27039 path!("/a"),
27040 json!({
27041 "main.rs": sample_text,
27042 }),
27043 )
27044 .await;
27045 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27046 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27047 let workspace = window
27048 .read_with(cx, |mw, _| mw.workspace().clone())
27049 .unwrap();
27050 let cx = &mut VisualTestContext::from_window(*window, cx);
27051 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27052 workspace.project().update(cx, |project, cx| {
27053 project.worktrees(cx).next().unwrap().read(cx).id()
27054 })
27055 });
27056
27057 let buffer = project
27058 .update(cx, |project, cx| {
27059 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27060 })
27061 .await
27062 .unwrap();
27063
27064 let (editor, cx) = cx.add_window_view(|window, cx| {
27065 Editor::new(
27066 EditorMode::full(),
27067 MultiBuffer::build_from_buffer(buffer, cx),
27068 Some(project.clone()),
27069 window,
27070 cx,
27071 )
27072 });
27073
27074 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27075 let abs_path = project.read_with(cx, |project, cx| {
27076 project
27077 .absolute_path(&project_path, cx)
27078 .map(Arc::from)
27079 .unwrap()
27080 });
27081
27082 // assert we can add breakpoint on the first line
27083 editor.update_in(cx, |editor, window, cx| {
27084 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27085 editor.move_to_end(&MoveToEnd, window, cx);
27086 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27087 });
27088
27089 let breakpoints = editor.update(cx, |editor, cx| {
27090 editor
27091 .breakpoint_store()
27092 .as_ref()
27093 .unwrap()
27094 .read(cx)
27095 .all_source_breakpoints(cx)
27096 });
27097
27098 assert_eq!(1, breakpoints.len());
27099 assert_breakpoint(
27100 &breakpoints,
27101 &abs_path,
27102 vec![
27103 (0, Breakpoint::new_standard()),
27104 (3, Breakpoint::new_standard()),
27105 ],
27106 );
27107
27108 editor.update_in(cx, |editor, window, cx| {
27109 editor.move_to_beginning(&MoveToBeginning, window, cx);
27110 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27111 });
27112
27113 let breakpoints = editor.update(cx, |editor, cx| {
27114 editor
27115 .breakpoint_store()
27116 .as_ref()
27117 .unwrap()
27118 .read(cx)
27119 .all_source_breakpoints(cx)
27120 });
27121
27122 assert_eq!(1, breakpoints.len());
27123 assert_breakpoint(
27124 &breakpoints,
27125 &abs_path,
27126 vec![(3, Breakpoint::new_standard())],
27127 );
27128
27129 editor.update_in(cx, |editor, window, cx| {
27130 editor.move_to_end(&MoveToEnd, window, cx);
27131 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27132 });
27133
27134 let breakpoints = editor.update(cx, |editor, cx| {
27135 editor
27136 .breakpoint_store()
27137 .as_ref()
27138 .unwrap()
27139 .read(cx)
27140 .all_source_breakpoints(cx)
27141 });
27142
27143 assert_eq!(0, breakpoints.len());
27144 assert_breakpoint(&breakpoints, &abs_path, vec![]);
27145}
27146
27147#[gpui::test]
27148async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
27149 init_test(cx, |_| {});
27150
27151 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27152
27153 let fs = FakeFs::new(cx.executor());
27154 fs.insert_tree(
27155 path!("/a"),
27156 json!({
27157 "main.rs": sample_text,
27158 }),
27159 )
27160 .await;
27161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27162 let (multi_workspace, cx) =
27163 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27164 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27165
27166 let worktree_id = workspace.update(cx, |workspace, cx| {
27167 workspace.project().update(cx, |project, cx| {
27168 project.worktrees(cx).next().unwrap().read(cx).id()
27169 })
27170 });
27171
27172 let buffer = project
27173 .update(cx, |project, cx| {
27174 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27175 })
27176 .await
27177 .unwrap();
27178
27179 let (editor, cx) = cx.add_window_view(|window, cx| {
27180 Editor::new(
27181 EditorMode::full(),
27182 MultiBuffer::build_from_buffer(buffer, cx),
27183 Some(project.clone()),
27184 window,
27185 cx,
27186 )
27187 });
27188
27189 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27190 let abs_path = project.read_with(cx, |project, cx| {
27191 project
27192 .absolute_path(&project_path, cx)
27193 .map(Arc::from)
27194 .unwrap()
27195 });
27196
27197 editor.update_in(cx, |editor, window, cx| {
27198 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
27199 });
27200
27201 let breakpoints = editor.update(cx, |editor, cx| {
27202 editor
27203 .breakpoint_store()
27204 .as_ref()
27205 .unwrap()
27206 .read(cx)
27207 .all_source_breakpoints(cx)
27208 });
27209
27210 assert_breakpoint(
27211 &breakpoints,
27212 &abs_path,
27213 vec![(0, Breakpoint::new_log("hello world"))],
27214 );
27215
27216 // Removing a log message from a log breakpoint should remove it
27217 editor.update_in(cx, |editor, window, cx| {
27218 add_log_breakpoint_at_cursor(editor, "", window, cx);
27219 });
27220
27221 let breakpoints = editor.update(cx, |editor, cx| {
27222 editor
27223 .breakpoint_store()
27224 .as_ref()
27225 .unwrap()
27226 .read(cx)
27227 .all_source_breakpoints(cx)
27228 });
27229
27230 assert_breakpoint(&breakpoints, &abs_path, vec![]);
27231
27232 editor.update_in(cx, |editor, window, cx| {
27233 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27234 editor.move_to_end(&MoveToEnd, window, cx);
27235 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27236 // Not adding a log message to a standard breakpoint shouldn't remove it
27237 add_log_breakpoint_at_cursor(editor, "", window, cx);
27238 });
27239
27240 let breakpoints = editor.update(cx, |editor, cx| {
27241 editor
27242 .breakpoint_store()
27243 .as_ref()
27244 .unwrap()
27245 .read(cx)
27246 .all_source_breakpoints(cx)
27247 });
27248
27249 assert_breakpoint(
27250 &breakpoints,
27251 &abs_path,
27252 vec![
27253 (0, Breakpoint::new_standard()),
27254 (3, Breakpoint::new_standard()),
27255 ],
27256 );
27257
27258 editor.update_in(cx, |editor, window, cx| {
27259 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
27260 });
27261
27262 let breakpoints = editor.update(cx, |editor, cx| {
27263 editor
27264 .breakpoint_store()
27265 .as_ref()
27266 .unwrap()
27267 .read(cx)
27268 .all_source_breakpoints(cx)
27269 });
27270
27271 assert_breakpoint(
27272 &breakpoints,
27273 &abs_path,
27274 vec![
27275 (0, Breakpoint::new_standard()),
27276 (3, Breakpoint::new_log("hello world")),
27277 ],
27278 );
27279
27280 editor.update_in(cx, |editor, window, cx| {
27281 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
27282 });
27283
27284 let breakpoints = editor.update(cx, |editor, cx| {
27285 editor
27286 .breakpoint_store()
27287 .as_ref()
27288 .unwrap()
27289 .read(cx)
27290 .all_source_breakpoints(cx)
27291 });
27292
27293 assert_breakpoint(
27294 &breakpoints,
27295 &abs_path,
27296 vec![
27297 (0, Breakpoint::new_standard()),
27298 (3, Breakpoint::new_log("hello Earth!!")),
27299 ],
27300 );
27301}
27302
27303/// This also tests that Editor::breakpoint_at_cursor_head is working properly
27304/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
27305/// or when breakpoints were placed out of order. This tests for a regression too
27306#[gpui::test]
27307async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
27308 init_test(cx, |_| {});
27309
27310 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27311 let fs = FakeFs::new(cx.executor());
27312 fs.insert_tree(
27313 path!("/a"),
27314 json!({
27315 "main.rs": sample_text,
27316 }),
27317 )
27318 .await;
27319 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27320 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27321 let cx = &mut VisualTestContext::from_window(*window, cx);
27322
27323 let fs = FakeFs::new(cx.executor());
27324 fs.insert_tree(
27325 path!("/a"),
27326 json!({
27327 "main.rs": sample_text,
27328 }),
27329 )
27330 .await;
27331 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27332 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27333 let workspace = window
27334 .read_with(cx, |mw, _| mw.workspace().clone())
27335 .unwrap();
27336 let cx = &mut VisualTestContext::from_window(*window, cx);
27337 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27338 workspace.project().update(cx, |project, cx| {
27339 project.worktrees(cx).next().unwrap().read(cx).id()
27340 })
27341 });
27342
27343 let buffer = project
27344 .update(cx, |project, cx| {
27345 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27346 })
27347 .await
27348 .unwrap();
27349
27350 let (editor, cx) = cx.add_window_view(|window, cx| {
27351 Editor::new(
27352 EditorMode::full(),
27353 MultiBuffer::build_from_buffer(buffer, cx),
27354 Some(project.clone()),
27355 window,
27356 cx,
27357 )
27358 });
27359
27360 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27361 let abs_path = project.read_with(cx, |project, cx| {
27362 project
27363 .absolute_path(&project_path, cx)
27364 .map(Arc::from)
27365 .unwrap()
27366 });
27367
27368 // assert we can add breakpoint on the first line
27369 editor.update_in(cx, |editor, window, cx| {
27370 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27371 editor.move_to_end(&MoveToEnd, window, cx);
27372 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27373 editor.move_up(&MoveUp, window, cx);
27374 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27375 });
27376
27377 let breakpoints = editor.update(cx, |editor, cx| {
27378 editor
27379 .breakpoint_store()
27380 .as_ref()
27381 .unwrap()
27382 .read(cx)
27383 .all_source_breakpoints(cx)
27384 });
27385
27386 assert_eq!(1, breakpoints.len());
27387 assert_breakpoint(
27388 &breakpoints,
27389 &abs_path,
27390 vec![
27391 (0, Breakpoint::new_standard()),
27392 (2, Breakpoint::new_standard()),
27393 (3, Breakpoint::new_standard()),
27394 ],
27395 );
27396
27397 editor.update_in(cx, |editor, window, cx| {
27398 editor.move_to_beginning(&MoveToBeginning, window, cx);
27399 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27400 editor.move_to_end(&MoveToEnd, window, cx);
27401 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27402 // Disabling a breakpoint that doesn't exist should do nothing
27403 editor.move_up(&MoveUp, window, cx);
27404 editor.move_up(&MoveUp, window, cx);
27405 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27406 });
27407
27408 let breakpoints = editor.update(cx, |editor, cx| {
27409 editor
27410 .breakpoint_store()
27411 .as_ref()
27412 .unwrap()
27413 .read(cx)
27414 .all_source_breakpoints(cx)
27415 });
27416
27417 let disable_breakpoint = {
27418 let mut bp = Breakpoint::new_standard();
27419 bp.state = BreakpointState::Disabled;
27420 bp
27421 };
27422
27423 assert_eq!(1, breakpoints.len());
27424 assert_breakpoint(
27425 &breakpoints,
27426 &abs_path,
27427 vec![
27428 (0, disable_breakpoint.clone()),
27429 (2, Breakpoint::new_standard()),
27430 (3, disable_breakpoint.clone()),
27431 ],
27432 );
27433
27434 editor.update_in(cx, |editor, window, cx| {
27435 editor.move_to_beginning(&MoveToBeginning, window, cx);
27436 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
27437 editor.move_to_end(&MoveToEnd, window, cx);
27438 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
27439 editor.move_up(&MoveUp, window, cx);
27440 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27441 });
27442
27443 let breakpoints = editor.update(cx, |editor, cx| {
27444 editor
27445 .breakpoint_store()
27446 .as_ref()
27447 .unwrap()
27448 .read(cx)
27449 .all_source_breakpoints(cx)
27450 });
27451
27452 assert_eq!(1, breakpoints.len());
27453 assert_breakpoint(
27454 &breakpoints,
27455 &abs_path,
27456 vec![
27457 (0, Breakpoint::new_standard()),
27458 (2, disable_breakpoint),
27459 (3, Breakpoint::new_standard()),
27460 ],
27461 );
27462}
27463
27464#[gpui::test]
27465async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
27466 init_test(cx, |_| {});
27467
27468 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27469 let fs = FakeFs::new(cx.executor());
27470 fs.insert_tree(
27471 path!("/a"),
27472 json!({
27473 "main.rs": sample_text,
27474 }),
27475 )
27476 .await;
27477 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27478 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27479 let workspace = window
27480 .read_with(cx, |mw, _| mw.workspace().clone())
27481 .unwrap();
27482 let cx = &mut VisualTestContext::from_window(*window, cx);
27483 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27484 workspace.project().update(cx, |project, cx| {
27485 project.worktrees(cx).next().unwrap().read(cx).id()
27486 })
27487 });
27488
27489 let buffer = project
27490 .update(cx, |project, cx| {
27491 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27492 })
27493 .await
27494 .unwrap();
27495
27496 let (editor, cx) = cx.add_window_view(|window, cx| {
27497 Editor::new(
27498 EditorMode::full(),
27499 MultiBuffer::build_from_buffer(buffer, cx),
27500 Some(project.clone()),
27501 window,
27502 cx,
27503 )
27504 });
27505
27506 // Simulate hovering over row 0 with no existing breakpoint.
27507 editor.update(cx, |editor, _cx| {
27508 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
27509 display_row: DisplayRow(0),
27510 is_active: true,
27511 collides_with_existing_breakpoint: false,
27512 });
27513 });
27514
27515 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
27516 editor.update_in(cx, |editor, window, cx| {
27517 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27518 });
27519 editor.update(cx, |editor, _cx| {
27520 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27521 assert!(
27522 indicator.collides_with_existing_breakpoint,
27523 "Adding a breakpoint on the hovered row should set collision to true"
27524 );
27525 });
27526
27527 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
27528 editor.update_in(cx, |editor, window, cx| {
27529 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27530 });
27531 editor.update(cx, |editor, _cx| {
27532 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27533 assert!(
27534 !indicator.collides_with_existing_breakpoint,
27535 "Removing a breakpoint on the hovered row should set collision to false"
27536 );
27537 });
27538
27539 // Now move cursor to row 2 while phantom indicator stays on row 0.
27540 editor.update_in(cx, |editor, window, cx| {
27541 editor.move_down(&MoveDown, window, cx);
27542 editor.move_down(&MoveDown, window, cx);
27543 });
27544
27545 // Ensure phantom indicator is still on row 0, not colliding.
27546 editor.update(cx, |editor, _cx| {
27547 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
27548 display_row: DisplayRow(0),
27549 is_active: true,
27550 collides_with_existing_breakpoint: false,
27551 });
27552 });
27553
27554 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
27555 editor.update_in(cx, |editor, window, cx| {
27556 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27557 });
27558 editor.update(cx, |editor, _cx| {
27559 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27560 assert!(
27561 !indicator.collides_with_existing_breakpoint,
27562 "Toggling a breakpoint on a different row should not affect the phantom indicator"
27563 );
27564 });
27565}
27566
27567#[gpui::test]
27568async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
27569 init_test(cx, |_| {});
27570 let capabilities = lsp::ServerCapabilities {
27571 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
27572 prepare_provider: Some(true),
27573 work_done_progress_options: Default::default(),
27574 })),
27575 ..Default::default()
27576 };
27577 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
27578
27579 cx.set_state(indoc! {"
27580 struct Fˇoo {}
27581 "});
27582
27583 cx.update_editor(|editor, _, cx| {
27584 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
27585 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
27586 editor.highlight_background(
27587 HighlightKey::DocumentHighlightRead,
27588 &[highlight_range],
27589 |_, theme| theme.colors().editor_document_highlight_read_background,
27590 cx,
27591 );
27592 });
27593
27594 let mut prepare_rename_handler = cx
27595 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
27596 move |_, _, _| async move {
27597 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
27598 start: lsp::Position {
27599 line: 0,
27600 character: 7,
27601 },
27602 end: lsp::Position {
27603 line: 0,
27604 character: 10,
27605 },
27606 })))
27607 },
27608 );
27609 let prepare_rename_task = cx
27610 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
27611 .expect("Prepare rename was not started");
27612 prepare_rename_handler.next().await.unwrap();
27613 prepare_rename_task.await.expect("Prepare rename failed");
27614
27615 let mut rename_handler =
27616 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
27617 let edit = lsp::TextEdit {
27618 range: lsp::Range {
27619 start: lsp::Position {
27620 line: 0,
27621 character: 7,
27622 },
27623 end: lsp::Position {
27624 line: 0,
27625 character: 10,
27626 },
27627 },
27628 new_text: "FooRenamed".to_string(),
27629 };
27630 Ok(Some(lsp::WorkspaceEdit::new(
27631 // Specify the same edit twice
27632 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
27633 )))
27634 });
27635 let rename_task = cx
27636 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
27637 .expect("Confirm rename was not started");
27638 rename_handler.next().await.unwrap();
27639 rename_task.await.expect("Confirm rename failed");
27640 cx.run_until_parked();
27641
27642 // Despite two edits, only one is actually applied as those are identical
27643 cx.assert_editor_state(indoc! {"
27644 struct FooRenamedˇ {}
27645 "});
27646}
27647
27648#[gpui::test]
27649async fn test_rename_without_prepare(cx: &mut TestAppContext) {
27650 init_test(cx, |_| {});
27651 // These capabilities indicate that the server does not support prepare rename.
27652 let capabilities = lsp::ServerCapabilities {
27653 rename_provider: Some(lsp::OneOf::Left(true)),
27654 ..Default::default()
27655 };
27656 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
27657
27658 cx.set_state(indoc! {"
27659 struct Fˇoo {}
27660 "});
27661
27662 cx.update_editor(|editor, _window, cx| {
27663 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
27664 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
27665 editor.highlight_background(
27666 HighlightKey::DocumentHighlightRead,
27667 &[highlight_range],
27668 |_, theme| theme.colors().editor_document_highlight_read_background,
27669 cx,
27670 );
27671 });
27672
27673 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
27674 .expect("Prepare rename was not started")
27675 .await
27676 .expect("Prepare rename failed");
27677
27678 let mut rename_handler =
27679 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
27680 let edit = lsp::TextEdit {
27681 range: lsp::Range {
27682 start: lsp::Position {
27683 line: 0,
27684 character: 7,
27685 },
27686 end: lsp::Position {
27687 line: 0,
27688 character: 10,
27689 },
27690 },
27691 new_text: "FooRenamed".to_string(),
27692 };
27693 Ok(Some(lsp::WorkspaceEdit::new(
27694 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
27695 )))
27696 });
27697 let rename_task = cx
27698 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
27699 .expect("Confirm rename was not started");
27700 rename_handler.next().await.unwrap();
27701 rename_task.await.expect("Confirm rename failed");
27702 cx.run_until_parked();
27703
27704 // Correct range is renamed, as `surrounding_word` is used to find it.
27705 cx.assert_editor_state(indoc! {"
27706 struct FooRenamedˇ {}
27707 "});
27708}
27709
27710#[gpui::test]
27711async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
27712 init_test(cx, |_| {});
27713 let mut cx = EditorTestContext::new(cx).await;
27714
27715 let language = Arc::new(
27716 Language::new(
27717 LanguageConfig::default(),
27718 Some(tree_sitter_html::LANGUAGE.into()),
27719 )
27720 .with_brackets_query(
27721 r#"
27722 ("<" @open "/>" @close)
27723 ("</" @open ">" @close)
27724 ("<" @open ">" @close)
27725 ("\"" @open "\"" @close)
27726 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
27727 "#,
27728 )
27729 .unwrap(),
27730 );
27731 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27732
27733 cx.set_state(indoc! {"
27734 <span>ˇ</span>
27735 "});
27736 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27737 cx.assert_editor_state(indoc! {"
27738 <span>
27739 ˇ
27740 </span>
27741 "});
27742
27743 cx.set_state(indoc! {"
27744 <span><span></span>ˇ</span>
27745 "});
27746 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27747 cx.assert_editor_state(indoc! {"
27748 <span><span></span>
27749 ˇ</span>
27750 "});
27751
27752 cx.set_state(indoc! {"
27753 <span>ˇ
27754 </span>
27755 "});
27756 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27757 cx.assert_editor_state(indoc! {"
27758 <span>
27759 ˇ
27760 </span>
27761 "});
27762}
27763
27764#[gpui::test(iterations = 10)]
27765async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
27766 init_test(cx, |_| {});
27767
27768 let fs = FakeFs::new(cx.executor());
27769 fs.insert_tree(
27770 path!("/dir"),
27771 json!({
27772 "a.ts": "a",
27773 }),
27774 )
27775 .await;
27776
27777 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
27778 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27779 let workspace = window
27780 .read_with(cx, |mw, _| mw.workspace().clone())
27781 .unwrap();
27782 let cx = &mut VisualTestContext::from_window(*window, cx);
27783
27784 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27785 language_registry.add(Arc::new(Language::new(
27786 LanguageConfig {
27787 name: "TypeScript".into(),
27788 matcher: LanguageMatcher {
27789 path_suffixes: vec!["ts".to_string()],
27790 ..Default::default()
27791 },
27792 ..Default::default()
27793 },
27794 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
27795 )));
27796 let mut fake_language_servers = language_registry.register_fake_lsp(
27797 "TypeScript",
27798 FakeLspAdapter {
27799 capabilities: lsp::ServerCapabilities {
27800 code_lens_provider: Some(lsp::CodeLensOptions {
27801 resolve_provider: Some(true),
27802 }),
27803 execute_command_provider: Some(lsp::ExecuteCommandOptions {
27804 commands: vec!["_the/command".to_string()],
27805 ..lsp::ExecuteCommandOptions::default()
27806 }),
27807 ..lsp::ServerCapabilities::default()
27808 },
27809 ..FakeLspAdapter::default()
27810 },
27811 );
27812
27813 let editor = workspace
27814 .update_in(cx, |workspace, window, cx| {
27815 workspace.open_abs_path(
27816 PathBuf::from(path!("/dir/a.ts")),
27817 OpenOptions::default(),
27818 window,
27819 cx,
27820 )
27821 })
27822 .await
27823 .unwrap()
27824 .downcast::<Editor>()
27825 .unwrap();
27826 cx.executor().run_until_parked();
27827
27828 let fake_server = fake_language_servers.next().await.unwrap();
27829
27830 let buffer = editor.update(cx, |editor, cx| {
27831 editor
27832 .buffer()
27833 .read(cx)
27834 .as_singleton()
27835 .expect("have opened a single file by path")
27836 });
27837
27838 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
27839 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
27840 drop(buffer_snapshot);
27841 let actions = cx
27842 .update_window(*window, |_, window, cx| {
27843 project.code_actions(&buffer, anchor..anchor, window, cx)
27844 })
27845 .unwrap();
27846
27847 fake_server
27848 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
27849 Ok(Some(vec![
27850 lsp::CodeLens {
27851 range: lsp::Range::default(),
27852 command: Some(lsp::Command {
27853 title: "Code lens command".to_owned(),
27854 command: "_the/command".to_owned(),
27855 arguments: None,
27856 }),
27857 data: None,
27858 },
27859 lsp::CodeLens {
27860 range: lsp::Range::default(),
27861 command: Some(lsp::Command {
27862 title: "Command not in capabilities".to_owned(),
27863 command: "not in capabilities".to_owned(),
27864 arguments: None,
27865 }),
27866 data: None,
27867 },
27868 lsp::CodeLens {
27869 range: lsp::Range {
27870 start: lsp::Position {
27871 line: 1,
27872 character: 1,
27873 },
27874 end: lsp::Position {
27875 line: 1,
27876 character: 1,
27877 },
27878 },
27879 command: Some(lsp::Command {
27880 title: "Command not in range".to_owned(),
27881 command: "_the/command".to_owned(),
27882 arguments: None,
27883 }),
27884 data: None,
27885 },
27886 ]))
27887 })
27888 .next()
27889 .await;
27890
27891 let actions = actions.await.unwrap();
27892 assert_eq!(
27893 actions.len(),
27894 1,
27895 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
27896 );
27897 let action = actions[0].clone();
27898 let apply = project.update(cx, |project, cx| {
27899 project.apply_code_action(buffer.clone(), action, true, cx)
27900 });
27901
27902 // Resolving the code action does not populate its edits. In absence of
27903 // edits, we must execute the given command.
27904 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
27905 |mut lens, _| async move {
27906 let lens_command = lens.command.as_mut().expect("should have a command");
27907 assert_eq!(lens_command.title, "Code lens command");
27908 lens_command.arguments = Some(vec![json!("the-argument")]);
27909 Ok(lens)
27910 },
27911 );
27912
27913 // While executing the command, the language server sends the editor
27914 // a `workspaceEdit` request.
27915 fake_server
27916 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
27917 let fake = fake_server.clone();
27918 move |params, _| {
27919 assert_eq!(params.command, "_the/command");
27920 let fake = fake.clone();
27921 async move {
27922 fake.server
27923 .request::<lsp::request::ApplyWorkspaceEdit>(
27924 lsp::ApplyWorkspaceEditParams {
27925 label: None,
27926 edit: lsp::WorkspaceEdit {
27927 changes: Some(
27928 [(
27929 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
27930 vec![lsp::TextEdit {
27931 range: lsp::Range::new(
27932 lsp::Position::new(0, 0),
27933 lsp::Position::new(0, 0),
27934 ),
27935 new_text: "X".into(),
27936 }],
27937 )]
27938 .into_iter()
27939 .collect(),
27940 ),
27941 ..lsp::WorkspaceEdit::default()
27942 },
27943 },
27944 DEFAULT_LSP_REQUEST_TIMEOUT,
27945 )
27946 .await
27947 .into_response()
27948 .unwrap();
27949 Ok(Some(json!(null)))
27950 }
27951 }
27952 })
27953 .next()
27954 .await;
27955
27956 // Applying the code lens command returns a project transaction containing the edits
27957 // sent by the language server in its `workspaceEdit` request.
27958 let transaction = apply.await.unwrap();
27959 assert!(transaction.0.contains_key(&buffer));
27960 buffer.update(cx, |buffer, cx| {
27961 assert_eq!(buffer.text(), "Xa");
27962 buffer.undo(cx);
27963 assert_eq!(buffer.text(), "a");
27964 });
27965
27966 let actions_after_edits = cx
27967 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
27968 .unwrap()
27969 .await;
27970 assert_eq!(
27971 actions, actions_after_edits,
27972 "For the same selection, same code lens actions should be returned"
27973 );
27974
27975 let _responses =
27976 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
27977 panic!("No more code lens requests are expected");
27978 });
27979 editor.update_in(cx, |editor, window, cx| {
27980 editor.select_all(&SelectAll, window, cx);
27981 });
27982 cx.executor().run_until_parked();
27983 let new_actions = cx
27984 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
27985 .unwrap()
27986 .await;
27987 assert_eq!(
27988 actions, new_actions,
27989 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
27990 );
27991}
27992
27993#[gpui::test]
27994async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
27995 init_test(cx, |_| {});
27996
27997 let fs = FakeFs::new(cx.executor());
27998 let main_text = r#"fn main() {
27999println!("1");
28000println!("2");
28001println!("3");
28002println!("4");
28003println!("5");
28004}"#;
28005 let lib_text = "mod foo {}";
28006 fs.insert_tree(
28007 path!("/a"),
28008 json!({
28009 "lib.rs": lib_text,
28010 "main.rs": main_text,
28011 }),
28012 )
28013 .await;
28014
28015 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28016 let (multi_workspace, cx) =
28017 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28018 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28019 let worktree_id = workspace.update(cx, |workspace, cx| {
28020 workspace.project().update(cx, |project, cx| {
28021 project.worktrees(cx).next().unwrap().read(cx).id()
28022 })
28023 });
28024
28025 let expected_ranges = vec![
28026 Point::new(0, 0)..Point::new(0, 0),
28027 Point::new(1, 0)..Point::new(1, 1),
28028 Point::new(2, 0)..Point::new(2, 2),
28029 Point::new(3, 0)..Point::new(3, 3),
28030 ];
28031
28032 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28033 let editor_1 = workspace
28034 .update_in(cx, |workspace, window, cx| {
28035 workspace.open_path(
28036 (worktree_id, rel_path("main.rs")),
28037 Some(pane_1.downgrade()),
28038 true,
28039 window,
28040 cx,
28041 )
28042 })
28043 .unwrap()
28044 .await
28045 .downcast::<Editor>()
28046 .unwrap();
28047 pane_1.update(cx, |pane, cx| {
28048 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28049 open_editor.update(cx, |editor, cx| {
28050 assert_eq!(
28051 editor.display_text(cx),
28052 main_text,
28053 "Original main.rs text on initial open",
28054 );
28055 assert_eq!(
28056 editor
28057 .selections
28058 .all::<Point>(&editor.display_snapshot(cx))
28059 .into_iter()
28060 .map(|s| s.range())
28061 .collect::<Vec<_>>(),
28062 vec![Point::zero()..Point::zero()],
28063 "Default selections on initial open",
28064 );
28065 })
28066 });
28067 editor_1.update_in(cx, |editor, window, cx| {
28068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28069 s.select_ranges(expected_ranges.clone());
28070 });
28071 });
28072
28073 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
28074 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
28075 });
28076 let editor_2 = workspace
28077 .update_in(cx, |workspace, window, cx| {
28078 workspace.open_path(
28079 (worktree_id, rel_path("main.rs")),
28080 Some(pane_2.downgrade()),
28081 true,
28082 window,
28083 cx,
28084 )
28085 })
28086 .unwrap()
28087 .await
28088 .downcast::<Editor>()
28089 .unwrap();
28090 pane_2.update(cx, |pane, cx| {
28091 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28092 open_editor.update(cx, |editor, cx| {
28093 assert_eq!(
28094 editor.display_text(cx),
28095 main_text,
28096 "Original main.rs text on initial open in another panel",
28097 );
28098 assert_eq!(
28099 editor
28100 .selections
28101 .all::<Point>(&editor.display_snapshot(cx))
28102 .into_iter()
28103 .map(|s| s.range())
28104 .collect::<Vec<_>>(),
28105 vec![Point::zero()..Point::zero()],
28106 "Default selections on initial open in another panel",
28107 );
28108 })
28109 });
28110
28111 editor_2.update_in(cx, |editor, window, cx| {
28112 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
28113 });
28114
28115 let _other_editor_1 = workspace
28116 .update_in(cx, |workspace, window, cx| {
28117 workspace.open_path(
28118 (worktree_id, rel_path("lib.rs")),
28119 Some(pane_1.downgrade()),
28120 true,
28121 window,
28122 cx,
28123 )
28124 })
28125 .unwrap()
28126 .await
28127 .downcast::<Editor>()
28128 .unwrap();
28129 pane_1
28130 .update_in(cx, |pane, window, cx| {
28131 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
28132 })
28133 .await
28134 .unwrap();
28135 drop(editor_1);
28136 pane_1.update(cx, |pane, cx| {
28137 pane.active_item()
28138 .unwrap()
28139 .downcast::<Editor>()
28140 .unwrap()
28141 .update(cx, |editor, cx| {
28142 assert_eq!(
28143 editor.display_text(cx),
28144 lib_text,
28145 "Other file should be open and active",
28146 );
28147 });
28148 assert_eq!(pane.items().count(), 1, "No other editors should be open");
28149 });
28150
28151 let _other_editor_2 = workspace
28152 .update_in(cx, |workspace, window, cx| {
28153 workspace.open_path(
28154 (worktree_id, rel_path("lib.rs")),
28155 Some(pane_2.downgrade()),
28156 true,
28157 window,
28158 cx,
28159 )
28160 })
28161 .unwrap()
28162 .await
28163 .downcast::<Editor>()
28164 .unwrap();
28165 pane_2
28166 .update_in(cx, |pane, window, cx| {
28167 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
28168 })
28169 .await
28170 .unwrap();
28171 drop(editor_2);
28172 pane_2.update(cx, |pane, cx| {
28173 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28174 open_editor.update(cx, |editor, cx| {
28175 assert_eq!(
28176 editor.display_text(cx),
28177 lib_text,
28178 "Other file should be open and active in another panel too",
28179 );
28180 });
28181 assert_eq!(
28182 pane.items().count(),
28183 1,
28184 "No other editors should be open in another pane",
28185 );
28186 });
28187
28188 let _editor_1_reopened = workspace
28189 .update_in(cx, |workspace, window, cx| {
28190 workspace.open_path(
28191 (worktree_id, rel_path("main.rs")),
28192 Some(pane_1.downgrade()),
28193 true,
28194 window,
28195 cx,
28196 )
28197 })
28198 .unwrap()
28199 .await
28200 .downcast::<Editor>()
28201 .unwrap();
28202 let _editor_2_reopened = workspace
28203 .update_in(cx, |workspace, window, cx| {
28204 workspace.open_path(
28205 (worktree_id, rel_path("main.rs")),
28206 Some(pane_2.downgrade()),
28207 true,
28208 window,
28209 cx,
28210 )
28211 })
28212 .unwrap()
28213 .await
28214 .downcast::<Editor>()
28215 .unwrap();
28216 pane_1.update(cx, |pane, cx| {
28217 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28218 open_editor.update(cx, |editor, cx| {
28219 assert_eq!(
28220 editor.display_text(cx),
28221 main_text,
28222 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
28223 );
28224 assert_eq!(
28225 editor
28226 .selections
28227 .all::<Point>(&editor.display_snapshot(cx))
28228 .into_iter()
28229 .map(|s| s.range())
28230 .collect::<Vec<_>>(),
28231 expected_ranges,
28232 "Previous editor in the 1st panel had selections and should get them restored on reopen",
28233 );
28234 })
28235 });
28236 pane_2.update(cx, |pane, cx| {
28237 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28238 open_editor.update(cx, |editor, cx| {
28239 assert_eq!(
28240 editor.display_text(cx),
28241 r#"fn main() {
28242⋯rintln!("1");
28243⋯intln!("2");
28244⋯ntln!("3");
28245println!("4");
28246println!("5");
28247}"#,
28248 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
28249 );
28250 assert_eq!(
28251 editor
28252 .selections
28253 .all::<Point>(&editor.display_snapshot(cx))
28254 .into_iter()
28255 .map(|s| s.range())
28256 .collect::<Vec<_>>(),
28257 vec![Point::zero()..Point::zero()],
28258 "Previous editor in the 2nd pane had no selections changed hence should restore none",
28259 );
28260 })
28261 });
28262}
28263
28264#[gpui::test]
28265async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
28266 init_test(cx, |_| {});
28267
28268 let fs = FakeFs::new(cx.executor());
28269 let main_text = r#"fn main() {
28270println!("1");
28271println!("2");
28272println!("3");
28273println!("4");
28274println!("5");
28275}"#;
28276 let lib_text = "mod foo {}";
28277 fs.insert_tree(
28278 path!("/a"),
28279 json!({
28280 "lib.rs": lib_text,
28281 "main.rs": main_text,
28282 }),
28283 )
28284 .await;
28285
28286 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28287 let (multi_workspace, cx) =
28288 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28289 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28290 let worktree_id = workspace.update(cx, |workspace, cx| {
28291 workspace.project().update(cx, |project, cx| {
28292 project.worktrees(cx).next().unwrap().read(cx).id()
28293 })
28294 });
28295
28296 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28297 let editor = workspace
28298 .update_in(cx, |workspace, window, cx| {
28299 workspace.open_path(
28300 (worktree_id, rel_path("main.rs")),
28301 Some(pane.downgrade()),
28302 true,
28303 window,
28304 cx,
28305 )
28306 })
28307 .unwrap()
28308 .await
28309 .downcast::<Editor>()
28310 .unwrap();
28311 pane.update(cx, |pane, cx| {
28312 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28313 open_editor.update(cx, |editor, cx| {
28314 assert_eq!(
28315 editor.display_text(cx),
28316 main_text,
28317 "Original main.rs text on initial open",
28318 );
28319 })
28320 });
28321 editor.update_in(cx, |editor, window, cx| {
28322 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
28323 });
28324
28325 cx.update_global(|store: &mut SettingsStore, cx| {
28326 store.update_user_settings(cx, |s| {
28327 s.workspace.restore_on_file_reopen = Some(false);
28328 });
28329 });
28330 editor.update_in(cx, |editor, window, cx| {
28331 editor.fold_ranges(
28332 vec![
28333 Point::new(1, 0)..Point::new(1, 1),
28334 Point::new(2, 0)..Point::new(2, 2),
28335 Point::new(3, 0)..Point::new(3, 3),
28336 ],
28337 false,
28338 window,
28339 cx,
28340 );
28341 });
28342 pane.update_in(cx, |pane, window, cx| {
28343 pane.close_all_items(&CloseAllItems::default(), window, cx)
28344 })
28345 .await
28346 .unwrap();
28347 pane.update(cx, |pane, _| {
28348 assert!(pane.active_item().is_none());
28349 });
28350 cx.update_global(|store: &mut SettingsStore, cx| {
28351 store.update_user_settings(cx, |s| {
28352 s.workspace.restore_on_file_reopen = Some(true);
28353 });
28354 });
28355
28356 let _editor_reopened = workspace
28357 .update_in(cx, |workspace, window, cx| {
28358 workspace.open_path(
28359 (worktree_id, rel_path("main.rs")),
28360 Some(pane.downgrade()),
28361 true,
28362 window,
28363 cx,
28364 )
28365 })
28366 .unwrap()
28367 .await
28368 .downcast::<Editor>()
28369 .unwrap();
28370 pane.update(cx, |pane, cx| {
28371 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28372 open_editor.update(cx, |editor, cx| {
28373 assert_eq!(
28374 editor.display_text(cx),
28375 main_text,
28376 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
28377 );
28378 })
28379 });
28380}
28381
28382#[gpui::test]
28383async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
28384 struct EmptyModalView {
28385 focus_handle: gpui::FocusHandle,
28386 }
28387 impl EventEmitter<DismissEvent> for EmptyModalView {}
28388 impl Render for EmptyModalView {
28389 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
28390 div()
28391 }
28392 }
28393 impl Focusable for EmptyModalView {
28394 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
28395 self.focus_handle.clone()
28396 }
28397 }
28398 impl workspace::ModalView for EmptyModalView {}
28399 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
28400 EmptyModalView {
28401 focus_handle: cx.focus_handle(),
28402 }
28403 }
28404
28405 init_test(cx, |_| {});
28406
28407 let fs = FakeFs::new(cx.executor());
28408 let project = Project::test(fs, [], cx).await;
28409 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28410 let workspace = window
28411 .read_with(cx, |mw, _| mw.workspace().clone())
28412 .unwrap();
28413 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
28414 let cx = &mut VisualTestContext::from_window(*window, cx);
28415 let editor = cx.new_window_entity(|window, cx| {
28416 Editor::new(
28417 EditorMode::full(),
28418 buffer,
28419 Some(project.clone()),
28420 window,
28421 cx,
28422 )
28423 });
28424 workspace.update_in(cx, |workspace, window, cx| {
28425 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
28426 });
28427
28428 editor.update_in(cx, |editor, window, cx| {
28429 editor.open_context_menu(&OpenContextMenu, window, cx);
28430 assert!(editor.mouse_context_menu.is_some());
28431 });
28432 workspace.update_in(cx, |workspace, window, cx| {
28433 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
28434 });
28435
28436 cx.read(|cx| {
28437 assert!(editor.read(cx).mouse_context_menu.is_none());
28438 });
28439}
28440
28441fn set_linked_edit_ranges(
28442 opening: (Point, Point),
28443 closing: (Point, Point),
28444 editor: &mut Editor,
28445 cx: &mut Context<Editor>,
28446) {
28447 let Some((buffer, _)) = editor
28448 .buffer
28449 .read(cx)
28450 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
28451 else {
28452 panic!("Failed to get buffer for selection position");
28453 };
28454 let buffer = buffer.read(cx);
28455 let buffer_id = buffer.remote_id();
28456 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
28457 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
28458 let mut linked_ranges = HashMap::default();
28459 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
28460 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
28461}
28462
28463#[gpui::test]
28464async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
28465 init_test(cx, |_| {});
28466
28467 let fs = FakeFs::new(cx.executor());
28468 fs.insert_file(path!("/file.html"), Default::default())
28469 .await;
28470
28471 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
28472
28473 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28474 let html_language = Arc::new(Language::new(
28475 LanguageConfig {
28476 name: "HTML".into(),
28477 matcher: LanguageMatcher {
28478 path_suffixes: vec!["html".to_string()],
28479 ..LanguageMatcher::default()
28480 },
28481 brackets: BracketPairConfig {
28482 pairs: vec![BracketPair {
28483 start: "<".into(),
28484 end: ">".into(),
28485 close: true,
28486 ..Default::default()
28487 }],
28488 ..Default::default()
28489 },
28490 ..Default::default()
28491 },
28492 Some(tree_sitter_html::LANGUAGE.into()),
28493 ));
28494 language_registry.add(html_language);
28495 let mut fake_servers = language_registry.register_fake_lsp(
28496 "HTML",
28497 FakeLspAdapter {
28498 capabilities: lsp::ServerCapabilities {
28499 completion_provider: Some(lsp::CompletionOptions {
28500 resolve_provider: Some(true),
28501 ..Default::default()
28502 }),
28503 ..Default::default()
28504 },
28505 ..Default::default()
28506 },
28507 );
28508
28509 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28510 let workspace = window
28511 .read_with(cx, |mw, _| mw.workspace().clone())
28512 .unwrap();
28513 let cx = &mut VisualTestContext::from_window(*window, cx);
28514
28515 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
28516 workspace.project().update(cx, |project, cx| {
28517 project.worktrees(cx).next().unwrap().read(cx).id()
28518 })
28519 });
28520
28521 project
28522 .update(cx, |project, cx| {
28523 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
28524 })
28525 .await
28526 .unwrap();
28527 let editor = workspace
28528 .update_in(cx, |workspace, window, cx| {
28529 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
28530 })
28531 .await
28532 .unwrap()
28533 .downcast::<Editor>()
28534 .unwrap();
28535
28536 let fake_server = fake_servers.next().await.unwrap();
28537 cx.run_until_parked();
28538 editor.update_in(cx, |editor, window, cx| {
28539 editor.set_text("<ad></ad>", window, cx);
28540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
28541 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
28542 });
28543 set_linked_edit_ranges(
28544 (Point::new(0, 1), Point::new(0, 3)),
28545 (Point::new(0, 6), Point::new(0, 8)),
28546 editor,
28547 cx,
28548 );
28549 });
28550 let mut completion_handle =
28551 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
28552 Ok(Some(lsp::CompletionResponse::Array(vec![
28553 lsp::CompletionItem {
28554 label: "head".to_string(),
28555 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
28556 lsp::InsertReplaceEdit {
28557 new_text: "head".to_string(),
28558 insert: lsp::Range::new(
28559 lsp::Position::new(0, 1),
28560 lsp::Position::new(0, 3),
28561 ),
28562 replace: lsp::Range::new(
28563 lsp::Position::new(0, 1),
28564 lsp::Position::new(0, 3),
28565 ),
28566 },
28567 )),
28568 ..Default::default()
28569 },
28570 ])))
28571 });
28572 editor.update_in(cx, |editor, window, cx| {
28573 editor.show_completions(&ShowCompletions, window, cx);
28574 });
28575 cx.run_until_parked();
28576 completion_handle.next().await.unwrap();
28577 editor.update(cx, |editor, _| {
28578 assert!(
28579 editor.context_menu_visible(),
28580 "Completion menu should be visible"
28581 );
28582 });
28583 editor.update_in(cx, |editor, window, cx| {
28584 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
28585 });
28586 cx.executor().run_until_parked();
28587 editor.update(cx, |editor, cx| {
28588 assert_eq!(editor.text(cx), "<head></head>");
28589 });
28590}
28591
28592#[gpui::test]
28593async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
28594 init_test(cx, |_| {});
28595
28596 let mut cx = EditorTestContext::new(cx).await;
28597 let language = Arc::new(Language::new(
28598 LanguageConfig {
28599 name: "TSX".into(),
28600 matcher: LanguageMatcher {
28601 path_suffixes: vec!["tsx".to_string()],
28602 ..LanguageMatcher::default()
28603 },
28604 brackets: BracketPairConfig {
28605 pairs: vec![BracketPair {
28606 start: "<".into(),
28607 end: ">".into(),
28608 close: true,
28609 ..Default::default()
28610 }],
28611 ..Default::default()
28612 },
28613 linked_edit_characters: HashSet::from_iter(['.']),
28614 ..Default::default()
28615 },
28616 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
28617 ));
28618 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28619
28620 // Test typing > does not extend linked pair
28621 cx.set_state("<divˇ<div></div>");
28622 cx.update_editor(|editor, _, cx| {
28623 set_linked_edit_ranges(
28624 (Point::new(0, 1), Point::new(0, 4)),
28625 (Point::new(0, 11), Point::new(0, 14)),
28626 editor,
28627 cx,
28628 );
28629 });
28630 cx.update_editor(|editor, window, cx| {
28631 editor.handle_input(">", window, cx);
28632 });
28633 cx.assert_editor_state("<div>ˇ<div></div>");
28634
28635 // Test typing . do extend linked pair
28636 cx.set_state("<Animatedˇ></Animated>");
28637 cx.update_editor(|editor, _, cx| {
28638 set_linked_edit_ranges(
28639 (Point::new(0, 1), Point::new(0, 9)),
28640 (Point::new(0, 12), Point::new(0, 20)),
28641 editor,
28642 cx,
28643 );
28644 });
28645 cx.update_editor(|editor, window, cx| {
28646 editor.handle_input(".", window, cx);
28647 });
28648 cx.assert_editor_state("<Animated.ˇ></Animated.>");
28649 cx.update_editor(|editor, _, cx| {
28650 set_linked_edit_ranges(
28651 (Point::new(0, 1), Point::new(0, 10)),
28652 (Point::new(0, 13), Point::new(0, 21)),
28653 editor,
28654 cx,
28655 );
28656 });
28657 cx.update_editor(|editor, window, cx| {
28658 editor.handle_input("V", window, cx);
28659 });
28660 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
28661}
28662
28663#[gpui::test]
28664async fn test_linked_edits_on_typing_dot_without_language_override(cx: &mut TestAppContext) {
28665 init_test(cx, |_| {});
28666
28667 let mut cx = EditorTestContext::new(cx).await;
28668 let language = Arc::new(Language::new(
28669 LanguageConfig {
28670 name: "HTML".into(),
28671 matcher: LanguageMatcher {
28672 path_suffixes: vec!["html".to_string()],
28673 ..LanguageMatcher::default()
28674 },
28675 brackets: BracketPairConfig {
28676 pairs: vec![BracketPair {
28677 start: "<".into(),
28678 end: ">".into(),
28679 close: true,
28680 ..Default::default()
28681 }],
28682 ..Default::default()
28683 },
28684 ..Default::default()
28685 },
28686 Some(tree_sitter_html::LANGUAGE.into()),
28687 ));
28688 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28689
28690 cx.set_state("<Tableˇ></Table>");
28691 cx.update_editor(|editor, _, cx| {
28692 set_linked_edit_ranges(
28693 (Point::new(0, 1), Point::new(0, 6)),
28694 (Point::new(0, 9), Point::new(0, 14)),
28695 editor,
28696 cx,
28697 );
28698 });
28699 cx.update_editor(|editor, window, cx| {
28700 editor.handle_input(".", window, cx);
28701 });
28702 cx.assert_editor_state("<Table.ˇ></Table.>");
28703}
28704
28705#[gpui::test]
28706async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
28707 init_test(cx, |_| {});
28708
28709 let fs = FakeFs::new(cx.executor());
28710 fs.insert_tree(
28711 path!("/root"),
28712 json!({
28713 "a": {
28714 "main.rs": "fn main() {}",
28715 },
28716 "foo": {
28717 "bar": {
28718 "external_file.rs": "pub mod external {}",
28719 }
28720 }
28721 }),
28722 )
28723 .await;
28724
28725 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
28726 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28727 language_registry.add(rust_lang());
28728 let _fake_servers = language_registry.register_fake_lsp(
28729 "Rust",
28730 FakeLspAdapter {
28731 ..FakeLspAdapter::default()
28732 },
28733 );
28734 let (multi_workspace, cx) =
28735 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28736 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28737 let worktree_id = workspace.update(cx, |workspace, cx| {
28738 workspace.project().update(cx, |project, cx| {
28739 project.worktrees(cx).next().unwrap().read(cx).id()
28740 })
28741 });
28742
28743 let assert_language_servers_count =
28744 |expected: usize, context: &str, cx: &mut VisualTestContext| {
28745 project.update(cx, |project, cx| {
28746 let current = project
28747 .lsp_store()
28748 .read(cx)
28749 .as_local()
28750 .unwrap()
28751 .language_servers
28752 .len();
28753 assert_eq!(expected, current, "{context}");
28754 });
28755 };
28756
28757 assert_language_servers_count(
28758 0,
28759 "No servers should be running before any file is open",
28760 cx,
28761 );
28762 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28763 let main_editor = workspace
28764 .update_in(cx, |workspace, window, cx| {
28765 workspace.open_path(
28766 (worktree_id, rel_path("main.rs")),
28767 Some(pane.downgrade()),
28768 true,
28769 window,
28770 cx,
28771 )
28772 })
28773 .unwrap()
28774 .await
28775 .downcast::<Editor>()
28776 .unwrap();
28777 pane.update(cx, |pane, cx| {
28778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28779 open_editor.update(cx, |editor, cx| {
28780 assert_eq!(
28781 editor.display_text(cx),
28782 "fn main() {}",
28783 "Original main.rs text on initial open",
28784 );
28785 });
28786 assert_eq!(open_editor, main_editor);
28787 });
28788 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
28789
28790 let external_editor = workspace
28791 .update_in(cx, |workspace, window, cx| {
28792 workspace.open_abs_path(
28793 PathBuf::from("/root/foo/bar/external_file.rs"),
28794 OpenOptions::default(),
28795 window,
28796 cx,
28797 )
28798 })
28799 .await
28800 .expect("opening external file")
28801 .downcast::<Editor>()
28802 .expect("downcasted external file's open element to editor");
28803 pane.update(cx, |pane, cx| {
28804 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28805 open_editor.update(cx, |editor, cx| {
28806 assert_eq!(
28807 editor.display_text(cx),
28808 "pub mod external {}",
28809 "External file is open now",
28810 );
28811 });
28812 assert_eq!(open_editor, external_editor);
28813 });
28814 assert_language_servers_count(
28815 1,
28816 "Second, external, *.rs file should join the existing server",
28817 cx,
28818 );
28819
28820 pane.update_in(cx, |pane, window, cx| {
28821 pane.close_active_item(&CloseActiveItem::default(), window, cx)
28822 })
28823 .await
28824 .unwrap();
28825 pane.update_in(cx, |pane, window, cx| {
28826 pane.navigate_backward(&Default::default(), window, cx);
28827 });
28828 cx.run_until_parked();
28829 pane.update(cx, |pane, cx| {
28830 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28831 open_editor.update(cx, |editor, cx| {
28832 assert_eq!(
28833 editor.display_text(cx),
28834 "pub mod external {}",
28835 "External file is open now",
28836 );
28837 });
28838 });
28839 assert_language_servers_count(
28840 1,
28841 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
28842 cx,
28843 );
28844
28845 cx.update(|_, cx| {
28846 workspace::reload(cx);
28847 });
28848 assert_language_servers_count(
28849 1,
28850 "After reloading the worktree with local and external files opened, only one project should be started",
28851 cx,
28852 );
28853}
28854
28855#[gpui::test]
28856async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
28857 init_test(cx, |_| {});
28858
28859 let mut cx = EditorTestContext::new(cx).await;
28860 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
28861 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28862
28863 // test cursor move to start of each line on tab
28864 // for `if`, `elif`, `else`, `while`, `with` and `for`
28865 cx.set_state(indoc! {"
28866 def main():
28867 ˇ for item in items:
28868 ˇ while item.active:
28869 ˇ if item.value > 10:
28870 ˇ continue
28871 ˇ elif item.value < 0:
28872 ˇ break
28873 ˇ else:
28874 ˇ with item.context() as ctx:
28875 ˇ yield count
28876 ˇ else:
28877 ˇ log('while else')
28878 ˇ else:
28879 ˇ log('for else')
28880 "});
28881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28882 cx.wait_for_autoindent_applied().await;
28883 cx.assert_editor_state(indoc! {"
28884 def main():
28885 ˇfor item in items:
28886 ˇwhile item.active:
28887 ˇif item.value > 10:
28888 ˇcontinue
28889 ˇelif item.value < 0:
28890 ˇbreak
28891 ˇelse:
28892 ˇwith item.context() as ctx:
28893 ˇyield count
28894 ˇelse:
28895 ˇlog('while else')
28896 ˇelse:
28897 ˇlog('for else')
28898 "});
28899 // test relative indent is preserved when tab
28900 // for `if`, `elif`, `else`, `while`, `with` and `for`
28901 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28902 cx.wait_for_autoindent_applied().await;
28903 cx.assert_editor_state(indoc! {"
28904 def main():
28905 ˇfor item in items:
28906 ˇwhile item.active:
28907 ˇif item.value > 10:
28908 ˇcontinue
28909 ˇelif item.value < 0:
28910 ˇbreak
28911 ˇelse:
28912 ˇwith item.context() as ctx:
28913 ˇyield count
28914 ˇelse:
28915 ˇlog('while else')
28916 ˇelse:
28917 ˇlog('for else')
28918 "});
28919
28920 // test cursor move to start of each line on tab
28921 // for `try`, `except`, `else`, `finally`, `match` and `def`
28922 cx.set_state(indoc! {"
28923 def main():
28924 ˇ try:
28925 ˇ fetch()
28926 ˇ except ValueError:
28927 ˇ handle_error()
28928 ˇ else:
28929 ˇ match value:
28930 ˇ case _:
28931 ˇ finally:
28932 ˇ def status():
28933 ˇ return 0
28934 "});
28935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28936 cx.wait_for_autoindent_applied().await;
28937 cx.assert_editor_state(indoc! {"
28938 def main():
28939 ˇtry:
28940 ˇfetch()
28941 ˇexcept ValueError:
28942 ˇhandle_error()
28943 ˇelse:
28944 ˇmatch value:
28945 ˇcase _:
28946 ˇfinally:
28947 ˇdef status():
28948 ˇreturn 0
28949 "});
28950 // test relative indent is preserved when tab
28951 // for `try`, `except`, `else`, `finally`, `match` and `def`
28952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28953 cx.wait_for_autoindent_applied().await;
28954 cx.assert_editor_state(indoc! {"
28955 def main():
28956 ˇtry:
28957 ˇfetch()
28958 ˇexcept ValueError:
28959 ˇhandle_error()
28960 ˇelse:
28961 ˇmatch value:
28962 ˇcase _:
28963 ˇfinally:
28964 ˇdef status():
28965 ˇreturn 0
28966 "});
28967}
28968
28969#[gpui::test]
28970async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
28971 init_test(cx, |_| {});
28972
28973 let mut cx = EditorTestContext::new(cx).await;
28974 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
28975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28976
28977 // test `else` auto outdents when typed inside `if` block
28978 cx.set_state(indoc! {"
28979 def main():
28980 if i == 2:
28981 return
28982 ˇ
28983 "});
28984 cx.update_editor(|editor, window, cx| {
28985 editor.handle_input("else:", window, cx);
28986 });
28987 cx.wait_for_autoindent_applied().await;
28988 cx.assert_editor_state(indoc! {"
28989 def main():
28990 if i == 2:
28991 return
28992 else:ˇ
28993 "});
28994
28995 // test `except` auto outdents when typed inside `try` block
28996 cx.set_state(indoc! {"
28997 def main():
28998 try:
28999 i = 2
29000 ˇ
29001 "});
29002 cx.update_editor(|editor, window, cx| {
29003 editor.handle_input("except:", window, cx);
29004 });
29005 cx.wait_for_autoindent_applied().await;
29006 cx.assert_editor_state(indoc! {"
29007 def main():
29008 try:
29009 i = 2
29010 except:ˇ
29011 "});
29012
29013 // test `else` auto outdents when typed inside `except` block
29014 cx.set_state(indoc! {"
29015 def main():
29016 try:
29017 i = 2
29018 except:
29019 j = 2
29020 ˇ
29021 "});
29022 cx.update_editor(|editor, window, cx| {
29023 editor.handle_input("else:", window, cx);
29024 });
29025 cx.wait_for_autoindent_applied().await;
29026 cx.assert_editor_state(indoc! {"
29027 def main():
29028 try:
29029 i = 2
29030 except:
29031 j = 2
29032 else:ˇ
29033 "});
29034
29035 // test `finally` auto outdents when typed inside `else` block
29036 cx.set_state(indoc! {"
29037 def main():
29038 try:
29039 i = 2
29040 except:
29041 j = 2
29042 else:
29043 k = 2
29044 ˇ
29045 "});
29046 cx.update_editor(|editor, window, cx| {
29047 editor.handle_input("finally:", window, cx);
29048 });
29049 cx.wait_for_autoindent_applied().await;
29050 cx.assert_editor_state(indoc! {"
29051 def main():
29052 try:
29053 i = 2
29054 except:
29055 j = 2
29056 else:
29057 k = 2
29058 finally:ˇ
29059 "});
29060
29061 // test `else` does not outdents when typed inside `except` block right after for block
29062 cx.set_state(indoc! {"
29063 def main():
29064 try:
29065 i = 2
29066 except:
29067 for i in range(n):
29068 pass
29069 ˇ
29070 "});
29071 cx.update_editor(|editor, window, cx| {
29072 editor.handle_input("else:", window, cx);
29073 });
29074 cx.wait_for_autoindent_applied().await;
29075 cx.assert_editor_state(indoc! {"
29076 def main():
29077 try:
29078 i = 2
29079 except:
29080 for i in range(n):
29081 pass
29082 else:ˇ
29083 "});
29084
29085 // test `finally` auto outdents when typed inside `else` block right after for block
29086 cx.set_state(indoc! {"
29087 def main():
29088 try:
29089 i = 2
29090 except:
29091 j = 2
29092 else:
29093 for i in range(n):
29094 pass
29095 ˇ
29096 "});
29097 cx.update_editor(|editor, window, cx| {
29098 editor.handle_input("finally:", window, cx);
29099 });
29100 cx.wait_for_autoindent_applied().await;
29101 cx.assert_editor_state(indoc! {"
29102 def main():
29103 try:
29104 i = 2
29105 except:
29106 j = 2
29107 else:
29108 for i in range(n):
29109 pass
29110 finally:ˇ
29111 "});
29112
29113 // test `except` outdents to inner "try" block
29114 cx.set_state(indoc! {"
29115 def main():
29116 try:
29117 i = 2
29118 if i == 2:
29119 try:
29120 i = 3
29121 ˇ
29122 "});
29123 cx.update_editor(|editor, window, cx| {
29124 editor.handle_input("except:", window, cx);
29125 });
29126 cx.wait_for_autoindent_applied().await;
29127 cx.assert_editor_state(indoc! {"
29128 def main():
29129 try:
29130 i = 2
29131 if i == 2:
29132 try:
29133 i = 3
29134 except:ˇ
29135 "});
29136
29137 // test `except` outdents to outer "try" block
29138 cx.set_state(indoc! {"
29139 def main():
29140 try:
29141 i = 2
29142 if i == 2:
29143 try:
29144 i = 3
29145 ˇ
29146 "});
29147 cx.update_editor(|editor, window, cx| {
29148 editor.handle_input("except:", window, cx);
29149 });
29150 cx.wait_for_autoindent_applied().await;
29151 cx.assert_editor_state(indoc! {"
29152 def main():
29153 try:
29154 i = 2
29155 if i == 2:
29156 try:
29157 i = 3
29158 except:ˇ
29159 "});
29160
29161 // test `else` stays at correct indent when typed after `for` block
29162 cx.set_state(indoc! {"
29163 def main():
29164 for i in range(10):
29165 if i == 3:
29166 break
29167 ˇ
29168 "});
29169 cx.update_editor(|editor, window, cx| {
29170 editor.handle_input("else:", window, cx);
29171 });
29172 cx.wait_for_autoindent_applied().await;
29173 cx.assert_editor_state(indoc! {"
29174 def main():
29175 for i in range(10):
29176 if i == 3:
29177 break
29178 else:ˇ
29179 "});
29180
29181 // test does not outdent on typing after line with square brackets
29182 cx.set_state(indoc! {"
29183 def f() -> list[str]:
29184 ˇ
29185 "});
29186 cx.update_editor(|editor, window, cx| {
29187 editor.handle_input("a", window, cx);
29188 });
29189 cx.wait_for_autoindent_applied().await;
29190 cx.assert_editor_state(indoc! {"
29191 def f() -> list[str]:
29192 aˇ
29193 "});
29194
29195 // test does not outdent on typing : after case keyword
29196 cx.set_state(indoc! {"
29197 match 1:
29198 caseˇ
29199 "});
29200 cx.update_editor(|editor, window, cx| {
29201 editor.handle_input(":", window, cx);
29202 });
29203 cx.wait_for_autoindent_applied().await;
29204 cx.assert_editor_state(indoc! {"
29205 match 1:
29206 case:ˇ
29207 "});
29208}
29209
29210#[gpui::test]
29211async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
29212 init_test(cx, |_| {});
29213 update_test_language_settings(cx, &|settings| {
29214 settings.defaults.extend_comment_on_newline = Some(false);
29215 });
29216 let mut cx = EditorTestContext::new(cx).await;
29217 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
29218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29219
29220 // test correct indent after newline on comment
29221 cx.set_state(indoc! {"
29222 # COMMENT:ˇ
29223 "});
29224 cx.update_editor(|editor, window, cx| {
29225 editor.newline(&Newline, window, cx);
29226 });
29227 cx.wait_for_autoindent_applied().await;
29228 cx.assert_editor_state(indoc! {"
29229 # COMMENT:
29230 ˇ
29231 "});
29232
29233 // test correct indent after newline in brackets
29234 cx.set_state(indoc! {"
29235 {ˇ}
29236 "});
29237 cx.update_editor(|editor, window, cx| {
29238 editor.newline(&Newline, window, cx);
29239 });
29240 cx.wait_for_autoindent_applied().await;
29241 cx.assert_editor_state(indoc! {"
29242 {
29243 ˇ
29244 }
29245 "});
29246
29247 cx.set_state(indoc! {"
29248 (ˇ)
29249 "});
29250 cx.update_editor(|editor, window, cx| {
29251 editor.newline(&Newline, window, cx);
29252 });
29253 cx.run_until_parked();
29254 cx.assert_editor_state(indoc! {"
29255 (
29256 ˇ
29257 )
29258 "});
29259
29260 // do not indent after empty lists or dictionaries
29261 cx.set_state(indoc! {"
29262 a = []ˇ
29263 "});
29264 cx.update_editor(|editor, window, cx| {
29265 editor.newline(&Newline, window, cx);
29266 });
29267 cx.run_until_parked();
29268 cx.assert_editor_state(indoc! {"
29269 a = []
29270 ˇ
29271 "});
29272}
29273
29274#[gpui::test]
29275async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
29276 init_test(cx, |_| {});
29277
29278 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
29279 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
29280 language_registry.add(markdown_lang());
29281 language_registry.add(python_lang);
29282
29283 let mut cx = EditorTestContext::new(cx).await;
29284 cx.update_buffer(|buffer, cx| {
29285 buffer.set_language_registry(language_registry);
29286 buffer.set_language(Some(markdown_lang()), cx);
29287 });
29288
29289 // Test that `else:` correctly outdents to match `if:` inside the Python code block
29290 cx.set_state(indoc! {"
29291 # Heading
29292
29293 ```python
29294 def main():
29295 if condition:
29296 pass
29297 ˇ
29298 ```
29299 "});
29300 cx.update_editor(|editor, window, cx| {
29301 editor.handle_input("else:", window, cx);
29302 });
29303 cx.run_until_parked();
29304 cx.assert_editor_state(indoc! {"
29305 # Heading
29306
29307 ```python
29308 def main():
29309 if condition:
29310 pass
29311 else:ˇ
29312 ```
29313 "});
29314}
29315
29316#[gpui::test]
29317async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
29318 init_test(cx, |_| {});
29319
29320 let mut cx = EditorTestContext::new(cx).await;
29321 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29322 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29323
29324 // test cursor move to start of each line on tab
29325 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
29326 cx.set_state(indoc! {"
29327 function main() {
29328 ˇ for item in $items; do
29329 ˇ while [ -n \"$item\" ]; do
29330 ˇ if [ \"$value\" -gt 10 ]; then
29331 ˇ continue
29332 ˇ elif [ \"$value\" -lt 0 ]; then
29333 ˇ break
29334 ˇ else
29335 ˇ echo \"$item\"
29336 ˇ fi
29337 ˇ done
29338 ˇ done
29339 ˇ}
29340 "});
29341 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29342 cx.wait_for_autoindent_applied().await;
29343 cx.assert_editor_state(indoc! {"
29344 function main() {
29345 ˇfor item in $items; do
29346 ˇwhile [ -n \"$item\" ]; do
29347 ˇif [ \"$value\" -gt 10 ]; then
29348 ˇcontinue
29349 ˇelif [ \"$value\" -lt 0 ]; then
29350 ˇbreak
29351 ˇelse
29352 ˇecho \"$item\"
29353 ˇfi
29354 ˇdone
29355 ˇdone
29356 ˇ}
29357 "});
29358 // test relative indent is preserved when tab
29359 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29360 cx.wait_for_autoindent_applied().await;
29361 cx.assert_editor_state(indoc! {"
29362 function main() {
29363 ˇfor item in $items; do
29364 ˇwhile [ -n \"$item\" ]; do
29365 ˇif [ \"$value\" -gt 10 ]; then
29366 ˇcontinue
29367 ˇelif [ \"$value\" -lt 0 ]; then
29368 ˇbreak
29369 ˇelse
29370 ˇecho \"$item\"
29371 ˇfi
29372 ˇdone
29373 ˇdone
29374 ˇ}
29375 "});
29376
29377 // test cursor move to start of each line on tab
29378 // for `case` statement with patterns
29379 cx.set_state(indoc! {"
29380 function handle() {
29381 ˇ case \"$1\" in
29382 ˇ start)
29383 ˇ echo \"a\"
29384 ˇ ;;
29385 ˇ stop)
29386 ˇ echo \"b\"
29387 ˇ ;;
29388 ˇ *)
29389 ˇ echo \"c\"
29390 ˇ ;;
29391 ˇ esac
29392 ˇ}
29393 "});
29394 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29395 cx.wait_for_autoindent_applied().await;
29396 cx.assert_editor_state(indoc! {"
29397 function handle() {
29398 ˇcase \"$1\" in
29399 ˇstart)
29400 ˇecho \"a\"
29401 ˇ;;
29402 ˇstop)
29403 ˇecho \"b\"
29404 ˇ;;
29405 ˇ*)
29406 ˇecho \"c\"
29407 ˇ;;
29408 ˇesac
29409 ˇ}
29410 "});
29411}
29412
29413#[gpui::test]
29414async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
29415 init_test(cx, |_| {});
29416
29417 let mut cx = EditorTestContext::new(cx).await;
29418 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29419 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29420
29421 // test indents on comment insert
29422 cx.set_state(indoc! {"
29423 function main() {
29424 ˇ for item in $items; do
29425 ˇ while [ -n \"$item\" ]; do
29426 ˇ if [ \"$value\" -gt 10 ]; then
29427 ˇ continue
29428 ˇ elif [ \"$value\" -lt 0 ]; then
29429 ˇ break
29430 ˇ else
29431 ˇ echo \"$item\"
29432 ˇ fi
29433 ˇ done
29434 ˇ done
29435 ˇ}
29436 "});
29437 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
29438 cx.wait_for_autoindent_applied().await;
29439 cx.assert_editor_state(indoc! {"
29440 function main() {
29441 #ˇ for item in $items; do
29442 #ˇ while [ -n \"$item\" ]; do
29443 #ˇ if [ \"$value\" -gt 10 ]; then
29444 #ˇ continue
29445 #ˇ elif [ \"$value\" -lt 0 ]; then
29446 #ˇ break
29447 #ˇ else
29448 #ˇ echo \"$item\"
29449 #ˇ fi
29450 #ˇ done
29451 #ˇ done
29452 #ˇ}
29453 "});
29454}
29455
29456#[gpui::test]
29457async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
29458 init_test(cx, |_| {});
29459
29460 let mut cx = EditorTestContext::new(cx).await;
29461 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29463
29464 // test `else` auto outdents when typed inside `if` block
29465 cx.set_state(indoc! {"
29466 if [ \"$1\" = \"test\" ]; then
29467 echo \"foo bar\"
29468 ˇ
29469 "});
29470 cx.update_editor(|editor, window, cx| {
29471 editor.handle_input("else", window, cx);
29472 });
29473 cx.wait_for_autoindent_applied().await;
29474 cx.assert_editor_state(indoc! {"
29475 if [ \"$1\" = \"test\" ]; then
29476 echo \"foo bar\"
29477 elseˇ
29478 "});
29479
29480 // test `elif` auto outdents when typed inside `if` block
29481 cx.set_state(indoc! {"
29482 if [ \"$1\" = \"test\" ]; then
29483 echo \"foo bar\"
29484 ˇ
29485 "});
29486 cx.update_editor(|editor, window, cx| {
29487 editor.handle_input("elif", window, cx);
29488 });
29489 cx.wait_for_autoindent_applied().await;
29490 cx.assert_editor_state(indoc! {"
29491 if [ \"$1\" = \"test\" ]; then
29492 echo \"foo bar\"
29493 elifˇ
29494 "});
29495
29496 // test `fi` auto outdents when typed inside `else` block
29497 cx.set_state(indoc! {"
29498 if [ \"$1\" = \"test\" ]; then
29499 echo \"foo bar\"
29500 else
29501 echo \"bar baz\"
29502 ˇ
29503 "});
29504 cx.update_editor(|editor, window, cx| {
29505 editor.handle_input("fi", window, cx);
29506 });
29507 cx.wait_for_autoindent_applied().await;
29508 cx.assert_editor_state(indoc! {"
29509 if [ \"$1\" = \"test\" ]; then
29510 echo \"foo bar\"
29511 else
29512 echo \"bar baz\"
29513 fiˇ
29514 "});
29515
29516 // test `done` auto outdents when typed inside `while` block
29517 cx.set_state(indoc! {"
29518 while read line; do
29519 echo \"$line\"
29520 ˇ
29521 "});
29522 cx.update_editor(|editor, window, cx| {
29523 editor.handle_input("done", window, cx);
29524 });
29525 cx.wait_for_autoindent_applied().await;
29526 cx.assert_editor_state(indoc! {"
29527 while read line; do
29528 echo \"$line\"
29529 doneˇ
29530 "});
29531
29532 // test `done` auto outdents when typed inside `for` block
29533 cx.set_state(indoc! {"
29534 for file in *.txt; do
29535 cat \"$file\"
29536 ˇ
29537 "});
29538 cx.update_editor(|editor, window, cx| {
29539 editor.handle_input("done", window, cx);
29540 });
29541 cx.wait_for_autoindent_applied().await;
29542 cx.assert_editor_state(indoc! {"
29543 for file in *.txt; do
29544 cat \"$file\"
29545 doneˇ
29546 "});
29547
29548 // test `esac` auto outdents when typed inside `case` block
29549 cx.set_state(indoc! {"
29550 case \"$1\" in
29551 start)
29552 echo \"foo bar\"
29553 ;;
29554 stop)
29555 echo \"bar baz\"
29556 ;;
29557 ˇ
29558 "});
29559 cx.update_editor(|editor, window, cx| {
29560 editor.handle_input("esac", window, cx);
29561 });
29562 cx.wait_for_autoindent_applied().await;
29563 cx.assert_editor_state(indoc! {"
29564 case \"$1\" in
29565 start)
29566 echo \"foo bar\"
29567 ;;
29568 stop)
29569 echo \"bar baz\"
29570 ;;
29571 esacˇ
29572 "});
29573
29574 // test `*)` auto outdents when typed inside `case` block
29575 cx.set_state(indoc! {"
29576 case \"$1\" in
29577 start)
29578 echo \"foo bar\"
29579 ;;
29580 ˇ
29581 "});
29582 cx.update_editor(|editor, window, cx| {
29583 editor.handle_input("*)", window, cx);
29584 });
29585 cx.wait_for_autoindent_applied().await;
29586 cx.assert_editor_state(indoc! {"
29587 case \"$1\" in
29588 start)
29589 echo \"foo bar\"
29590 ;;
29591 *)ˇ
29592 "});
29593
29594 // test `fi` outdents to correct level with nested if blocks
29595 cx.set_state(indoc! {"
29596 if [ \"$1\" = \"test\" ]; then
29597 echo \"outer if\"
29598 if [ \"$2\" = \"debug\" ]; then
29599 echo \"inner if\"
29600 ˇ
29601 "});
29602 cx.update_editor(|editor, window, cx| {
29603 editor.handle_input("fi", window, cx);
29604 });
29605 cx.wait_for_autoindent_applied().await;
29606 cx.assert_editor_state(indoc! {"
29607 if [ \"$1\" = \"test\" ]; then
29608 echo \"outer if\"
29609 if [ \"$2\" = \"debug\" ]; then
29610 echo \"inner if\"
29611 fiˇ
29612 "});
29613}
29614
29615#[gpui::test]
29616async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
29617 init_test(cx, |_| {});
29618 update_test_language_settings(cx, &|settings| {
29619 settings.defaults.extend_comment_on_newline = Some(false);
29620 });
29621 let mut cx = EditorTestContext::new(cx).await;
29622 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29623 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29624
29625 // test correct indent after newline on comment
29626 cx.set_state(indoc! {"
29627 # COMMENT:ˇ
29628 "});
29629 cx.update_editor(|editor, window, cx| {
29630 editor.newline(&Newline, window, cx);
29631 });
29632 cx.wait_for_autoindent_applied().await;
29633 cx.assert_editor_state(indoc! {"
29634 # COMMENT:
29635 ˇ
29636 "});
29637
29638 // test correct indent after newline after `then`
29639 cx.set_state(indoc! {"
29640
29641 if [ \"$1\" = \"test\" ]; thenˇ
29642 "});
29643 cx.update_editor(|editor, window, cx| {
29644 editor.newline(&Newline, window, cx);
29645 });
29646 cx.wait_for_autoindent_applied().await;
29647 cx.assert_editor_state(indoc! {"
29648
29649 if [ \"$1\" = \"test\" ]; then
29650 ˇ
29651 "});
29652
29653 // test correct indent after newline after `else`
29654 cx.set_state(indoc! {"
29655 if [ \"$1\" = \"test\" ]; then
29656 elseˇ
29657 "});
29658 cx.update_editor(|editor, window, cx| {
29659 editor.newline(&Newline, window, cx);
29660 });
29661 cx.wait_for_autoindent_applied().await;
29662 cx.assert_editor_state(indoc! {"
29663 if [ \"$1\" = \"test\" ]; then
29664 else
29665 ˇ
29666 "});
29667
29668 // test correct indent after newline after `elif`
29669 cx.set_state(indoc! {"
29670 if [ \"$1\" = \"test\" ]; then
29671 elifˇ
29672 "});
29673 cx.update_editor(|editor, window, cx| {
29674 editor.newline(&Newline, window, cx);
29675 });
29676 cx.wait_for_autoindent_applied().await;
29677 cx.assert_editor_state(indoc! {"
29678 if [ \"$1\" = \"test\" ]; then
29679 elif
29680 ˇ
29681 "});
29682
29683 // test correct indent after newline after `do`
29684 cx.set_state(indoc! {"
29685 for file in *.txt; doˇ
29686 "});
29687 cx.update_editor(|editor, window, cx| {
29688 editor.newline(&Newline, window, cx);
29689 });
29690 cx.wait_for_autoindent_applied().await;
29691 cx.assert_editor_state(indoc! {"
29692 for file in *.txt; do
29693 ˇ
29694 "});
29695
29696 // test correct indent after newline after case pattern
29697 cx.set_state(indoc! {"
29698 case \"$1\" in
29699 start)ˇ
29700 "});
29701 cx.update_editor(|editor, window, cx| {
29702 editor.newline(&Newline, window, cx);
29703 });
29704 cx.wait_for_autoindent_applied().await;
29705 cx.assert_editor_state(indoc! {"
29706 case \"$1\" in
29707 start)
29708 ˇ
29709 "});
29710
29711 // test correct indent after newline after case pattern
29712 cx.set_state(indoc! {"
29713 case \"$1\" in
29714 start)
29715 ;;
29716 *)ˇ
29717 "});
29718 cx.update_editor(|editor, window, cx| {
29719 editor.newline(&Newline, window, cx);
29720 });
29721 cx.wait_for_autoindent_applied().await;
29722 cx.assert_editor_state(indoc! {"
29723 case \"$1\" in
29724 start)
29725 ;;
29726 *)
29727 ˇ
29728 "});
29729
29730 // test correct indent after newline after function opening brace
29731 cx.set_state(indoc! {"
29732 function test() {ˇ}
29733 "});
29734 cx.update_editor(|editor, window, cx| {
29735 editor.newline(&Newline, window, cx);
29736 });
29737 cx.wait_for_autoindent_applied().await;
29738 cx.assert_editor_state(indoc! {"
29739 function test() {
29740 ˇ
29741 }
29742 "});
29743
29744 // test no extra indent after semicolon on same line
29745 cx.set_state(indoc! {"
29746 echo \"test\";ˇ
29747 "});
29748 cx.update_editor(|editor, window, cx| {
29749 editor.newline(&Newline, window, cx);
29750 });
29751 cx.wait_for_autoindent_applied().await;
29752 cx.assert_editor_state(indoc! {"
29753 echo \"test\";
29754 ˇ
29755 "});
29756}
29757
29758fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
29759 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
29760 point..point
29761}
29762
29763#[track_caller]
29764fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
29765 let (text, ranges) = marked_text_ranges(marked_text, true);
29766 assert_eq!(editor.text(cx), text);
29767 assert_eq!(
29768 editor.selections.ranges(&editor.display_snapshot(cx)),
29769 ranges
29770 .iter()
29771 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
29772 .collect::<Vec<_>>(),
29773 "Assert selections are {}",
29774 marked_text
29775 );
29776}
29777
29778pub fn handle_signature_help_request(
29779 cx: &mut EditorLspTestContext,
29780 mocked_response: lsp::SignatureHelp,
29781) -> impl Future<Output = ()> + use<> {
29782 let mut request =
29783 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
29784 let mocked_response = mocked_response.clone();
29785 async move { Ok(Some(mocked_response)) }
29786 });
29787
29788 async move {
29789 request.next().await;
29790 }
29791}
29792
29793#[track_caller]
29794pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
29795 cx.update_editor(|editor, _, _| {
29796 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
29797 let entries = menu.entries.borrow();
29798 let entries = entries
29799 .iter()
29800 .map(|entry| entry.string.as_str())
29801 .collect::<Vec<_>>();
29802 assert_eq!(entries, expected);
29803 } else {
29804 panic!("Expected completions menu");
29805 }
29806 });
29807}
29808
29809#[gpui::test]
29810async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
29811 init_test(cx, |_| {});
29812 let mut cx = EditorLspTestContext::new_rust(
29813 lsp::ServerCapabilities {
29814 completion_provider: Some(lsp::CompletionOptions {
29815 ..Default::default()
29816 }),
29817 ..Default::default()
29818 },
29819 cx,
29820 )
29821 .await;
29822 cx.lsp
29823 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
29824 Ok(Some(lsp::CompletionResponse::Array(vec![
29825 lsp::CompletionItem {
29826 label: "unsafe".into(),
29827 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
29828 range: lsp::Range {
29829 start: lsp::Position {
29830 line: 0,
29831 character: 9,
29832 },
29833 end: lsp::Position {
29834 line: 0,
29835 character: 11,
29836 },
29837 },
29838 new_text: "unsafe".to_string(),
29839 })),
29840 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
29841 ..Default::default()
29842 },
29843 ])))
29844 });
29845
29846 cx.update_editor(|editor, _, cx| {
29847 editor.project().unwrap().update(cx, |project, cx| {
29848 project.snippets().update(cx, |snippets, _cx| {
29849 snippets.add_snippet_for_test(
29850 None,
29851 PathBuf::from("test_snippets.json"),
29852 vec![
29853 Arc::new(project::snippet_provider::Snippet {
29854 prefix: vec![
29855 "unlimited word count".to_string(),
29856 "unlimit word count".to_string(),
29857 "unlimited unknown".to_string(),
29858 ],
29859 body: "this is many words".to_string(),
29860 description: Some("description".to_string()),
29861 name: "multi-word snippet test".to_string(),
29862 }),
29863 Arc::new(project::snippet_provider::Snippet {
29864 prefix: vec!["unsnip".to_string(), "@few".to_string()],
29865 body: "fewer words".to_string(),
29866 description: Some("alt description".to_string()),
29867 name: "other name".to_string(),
29868 }),
29869 Arc::new(project::snippet_provider::Snippet {
29870 prefix: vec!["ab aa".to_string()],
29871 body: "abcd".to_string(),
29872 description: None,
29873 name: "alphabet".to_string(),
29874 }),
29875 ],
29876 );
29877 });
29878 })
29879 });
29880
29881 let get_completions = |cx: &mut EditorLspTestContext| {
29882 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
29883 Some(CodeContextMenu::Completions(context_menu)) => {
29884 let entries = context_menu.entries.borrow();
29885 entries
29886 .iter()
29887 .map(|entry| entry.string.clone())
29888 .collect_vec()
29889 }
29890 _ => vec![],
29891 })
29892 };
29893
29894 // snippets:
29895 // @foo
29896 // foo bar
29897 //
29898 // when typing:
29899 //
29900 // when typing:
29901 // - if I type a symbol "open the completions with snippets only"
29902 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
29903 //
29904 // stuff we need:
29905 // - filtering logic change?
29906 // - remember how far back the completion started.
29907
29908 let test_cases: &[(&str, &[&str])] = &[
29909 (
29910 "un",
29911 &[
29912 "unsafe",
29913 "unlimit word count",
29914 "unlimited unknown",
29915 "unlimited word count",
29916 "unsnip",
29917 ],
29918 ),
29919 (
29920 "u ",
29921 &[
29922 "unlimit word count",
29923 "unlimited unknown",
29924 "unlimited word count",
29925 ],
29926 ),
29927 ("u a", &["ab aa", "unsafe"]), // unsAfe
29928 (
29929 "u u",
29930 &[
29931 "unsafe",
29932 "unlimit word count",
29933 "unlimited unknown", // ranked highest among snippets
29934 "unlimited word count",
29935 "unsnip",
29936 ],
29937 ),
29938 ("uw c", &["unlimit word count", "unlimited word count"]),
29939 (
29940 "u w",
29941 &[
29942 "unlimit word count",
29943 "unlimited word count",
29944 "unlimited unknown",
29945 ],
29946 ),
29947 ("u w ", &["unlimit word count", "unlimited word count"]),
29948 (
29949 "u ",
29950 &[
29951 "unlimit word count",
29952 "unlimited unknown",
29953 "unlimited word count",
29954 ],
29955 ),
29956 ("wor", &[]),
29957 ("uf", &["unsafe"]),
29958 ("af", &["unsafe"]),
29959 ("afu", &[]),
29960 (
29961 "ue",
29962 &["unsafe", "unlimited unknown", "unlimited word count"],
29963 ),
29964 ("@", &["@few"]),
29965 ("@few", &["@few"]),
29966 ("@ ", &[]),
29967 ("a@", &["@few"]),
29968 ("a@f", &["@few", "unsafe"]),
29969 ("a@fw", &["@few"]),
29970 ("a", &["ab aa", "unsafe"]),
29971 ("aa", &["ab aa"]),
29972 ("aaa", &["ab aa"]),
29973 ("ab", &["ab aa"]),
29974 ("ab ", &["ab aa"]),
29975 ("ab a", &["ab aa", "unsafe"]),
29976 ("ab ab", &["ab aa"]),
29977 ("ab ab aa", &["ab aa"]),
29978 ];
29979
29980 for &(input_to_simulate, expected_completions) in test_cases {
29981 cx.set_state("fn a() { ˇ }\n");
29982 for c in input_to_simulate.split("") {
29983 cx.simulate_input(c);
29984 cx.run_until_parked();
29985 }
29986 let expected_completions = expected_completions
29987 .iter()
29988 .map(|s| s.to_string())
29989 .collect_vec();
29990 assert_eq!(
29991 get_completions(&mut cx),
29992 expected_completions,
29993 "< actual / expected >, input = {input_to_simulate:?}",
29994 );
29995 }
29996}
29997
29998/// Handle completion request passing a marked string specifying where the completion
29999/// should be triggered from using '|' character, what range should be replaced, and what completions
30000/// should be returned using '<' and '>' to delimit the range.
30001///
30002/// Also see `handle_completion_request_with_insert_and_replace`.
30003#[track_caller]
30004pub fn handle_completion_request(
30005 marked_string: &str,
30006 completions: Vec<&'static str>,
30007 is_incomplete: bool,
30008 counter: Arc<AtomicUsize>,
30009 cx: &mut EditorLspTestContext,
30010) -> impl Future<Output = ()> {
30011 let complete_from_marker: TextRangeMarker = '|'.into();
30012 let replace_range_marker: TextRangeMarker = ('<', '>').into();
30013 let (_, mut marked_ranges) = marked_text_ranges_by(
30014 marked_string,
30015 vec![complete_from_marker.clone(), replace_range_marker.clone()],
30016 );
30017
30018 let complete_from_position = cx.to_lsp(MultiBufferOffset(
30019 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
30020 ));
30021 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
30022 let replace_range =
30023 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
30024
30025 let mut request =
30026 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
30027 let completions = completions.clone();
30028 counter.fetch_add(1, atomic::Ordering::Release);
30029 async move {
30030 assert_eq!(params.text_document_position.text_document.uri, url.clone());
30031 assert_eq!(
30032 params.text_document_position.position,
30033 complete_from_position
30034 );
30035 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
30036 is_incomplete,
30037 item_defaults: None,
30038 items: completions
30039 .iter()
30040 .map(|completion_text| lsp::CompletionItem {
30041 label: completion_text.to_string(),
30042 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
30043 range: replace_range,
30044 new_text: completion_text.to_string(),
30045 })),
30046 ..Default::default()
30047 })
30048 .collect(),
30049 })))
30050 }
30051 });
30052
30053 async move {
30054 request.next().await;
30055 }
30056}
30057
30058/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
30059/// given instead, which also contains an `insert` range.
30060///
30061/// This function uses markers to define ranges:
30062/// - `|` marks the cursor position
30063/// - `<>` marks the replace range
30064/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
30065pub fn handle_completion_request_with_insert_and_replace(
30066 cx: &mut EditorLspTestContext,
30067 marked_string: &str,
30068 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
30069 counter: Arc<AtomicUsize>,
30070) -> impl Future<Output = ()> {
30071 let complete_from_marker: TextRangeMarker = '|'.into();
30072 let replace_range_marker: TextRangeMarker = ('<', '>').into();
30073 let insert_range_marker: TextRangeMarker = ('{', '}').into();
30074
30075 let (_, mut marked_ranges) = marked_text_ranges_by(
30076 marked_string,
30077 vec![
30078 complete_from_marker.clone(),
30079 replace_range_marker.clone(),
30080 insert_range_marker.clone(),
30081 ],
30082 );
30083
30084 let complete_from_position = cx.to_lsp(MultiBufferOffset(
30085 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
30086 ));
30087 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
30088 let replace_range =
30089 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
30090
30091 let insert_range = match marked_ranges.remove(&insert_range_marker) {
30092 Some(ranges) if !ranges.is_empty() => {
30093 let range1 = ranges[0].clone();
30094 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
30095 }
30096 _ => lsp::Range {
30097 start: replace_range.start,
30098 end: complete_from_position,
30099 },
30100 };
30101
30102 let mut request =
30103 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
30104 let completions = completions.clone();
30105 counter.fetch_add(1, atomic::Ordering::Release);
30106 async move {
30107 assert_eq!(params.text_document_position.text_document.uri, url.clone());
30108 assert_eq!(
30109 params.text_document_position.position, complete_from_position,
30110 "marker `|` position doesn't match",
30111 );
30112 Ok(Some(lsp::CompletionResponse::Array(
30113 completions
30114 .iter()
30115 .map(|(label, new_text)| lsp::CompletionItem {
30116 label: label.to_string(),
30117 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
30118 lsp::InsertReplaceEdit {
30119 insert: insert_range,
30120 replace: replace_range,
30121 new_text: new_text.to_string(),
30122 },
30123 )),
30124 ..Default::default()
30125 })
30126 .collect(),
30127 )))
30128 }
30129 });
30130
30131 async move {
30132 request.next().await;
30133 }
30134}
30135
30136fn handle_resolve_completion_request(
30137 cx: &mut EditorLspTestContext,
30138 edits: Option<Vec<(&'static str, &'static str)>>,
30139) -> impl Future<Output = ()> {
30140 let edits = edits.map(|edits| {
30141 edits
30142 .iter()
30143 .map(|(marked_string, new_text)| {
30144 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
30145 let replace_range = cx.to_lsp_range(
30146 MultiBufferOffset(marked_ranges[0].start)
30147 ..MultiBufferOffset(marked_ranges[0].end),
30148 );
30149 lsp::TextEdit::new(replace_range, new_text.to_string())
30150 })
30151 .collect::<Vec<_>>()
30152 });
30153
30154 let mut request =
30155 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
30156 let edits = edits.clone();
30157 async move {
30158 Ok(lsp::CompletionItem {
30159 additional_text_edits: edits,
30160 ..Default::default()
30161 })
30162 }
30163 });
30164
30165 async move {
30166 request.next().await;
30167 }
30168}
30169
30170pub(crate) fn update_test_language_settings(
30171 cx: &mut TestAppContext,
30172 f: &dyn Fn(&mut AllLanguageSettingsContent),
30173) {
30174 cx.update(|cx| {
30175 SettingsStore::update_global(cx, |store, cx| {
30176 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
30177 f(&mut settings.project.all_languages)
30178 });
30179 });
30180 });
30181}
30182
30183pub(crate) fn update_test_project_settings(
30184 cx: &mut TestAppContext,
30185 f: &dyn Fn(&mut ProjectSettingsContent),
30186) {
30187 cx.update(|cx| {
30188 SettingsStore::update_global(cx, |store, cx| {
30189 store.update_user_settings(cx, |settings| f(&mut settings.project));
30190 });
30191 });
30192}
30193
30194pub(crate) fn update_test_editor_settings(
30195 cx: &mut TestAppContext,
30196 f: &dyn Fn(&mut EditorSettingsContent),
30197) {
30198 cx.update(|cx| {
30199 SettingsStore::update_global(cx, |store, cx| {
30200 store.update_user_settings(cx, |settings| f(&mut settings.editor));
30201 })
30202 })
30203}
30204
30205pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
30206 cx.update(|cx| {
30207 assets::Assets.load_test_fonts(cx);
30208 let store = SettingsStore::test(cx);
30209 cx.set_global(store);
30210 theme_settings::init(theme::LoadThemes::JustBase, cx);
30211 release_channel::init(semver::Version::new(0, 0, 0), cx);
30212 crate::init(cx);
30213 });
30214 zlog::init_test();
30215 update_test_language_settings(cx, &f);
30216}
30217
30218#[track_caller]
30219fn assert_hunk_revert(
30220 not_reverted_text_with_selections: &str,
30221 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
30222 expected_reverted_text_with_selections: &str,
30223 base_text: &str,
30224 cx: &mut EditorLspTestContext,
30225) {
30226 cx.set_state(not_reverted_text_with_selections);
30227 cx.set_head_text(base_text);
30228 cx.executor().run_until_parked();
30229
30230 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
30231 let snapshot = editor.snapshot(window, cx);
30232 let reverted_hunk_statuses = snapshot
30233 .buffer_snapshot()
30234 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
30235 .map(|hunk| hunk.status().kind)
30236 .collect::<Vec<_>>();
30237
30238 editor.git_restore(&Default::default(), window, cx);
30239 reverted_hunk_statuses
30240 });
30241 cx.executor().run_until_parked();
30242 cx.assert_editor_state(expected_reverted_text_with_selections);
30243 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
30244}
30245
30246#[gpui::test(iterations = 10)]
30247async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
30248 init_test(cx, |_| {});
30249
30250 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
30251 let counter = diagnostic_requests.clone();
30252
30253 let fs = FakeFs::new(cx.executor());
30254 fs.insert_tree(
30255 path!("/a"),
30256 json!({
30257 "first.rs": "fn main() { let a = 5; }",
30258 "second.rs": "// Test file",
30259 }),
30260 )
30261 .await;
30262
30263 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
30264 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30265 let workspace = window
30266 .read_with(cx, |mw, _| mw.workspace().clone())
30267 .unwrap();
30268 let cx = &mut VisualTestContext::from_window(*window, cx);
30269
30270 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30271 language_registry.add(rust_lang());
30272 let mut fake_servers = language_registry.register_fake_lsp(
30273 "Rust",
30274 FakeLspAdapter {
30275 capabilities: lsp::ServerCapabilities {
30276 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
30277 lsp::DiagnosticOptions {
30278 identifier: None,
30279 inter_file_dependencies: true,
30280 workspace_diagnostics: true,
30281 work_done_progress_options: Default::default(),
30282 },
30283 )),
30284 ..Default::default()
30285 },
30286 ..Default::default()
30287 },
30288 );
30289
30290 let editor = workspace
30291 .update_in(cx, |workspace, window, cx| {
30292 workspace.open_abs_path(
30293 PathBuf::from(path!("/a/first.rs")),
30294 OpenOptions::default(),
30295 window,
30296 cx,
30297 )
30298 })
30299 .await
30300 .unwrap()
30301 .downcast::<Editor>()
30302 .unwrap();
30303 let fake_server = fake_servers.next().await.unwrap();
30304 let server_id = fake_server.server.server_id();
30305 let mut first_request = fake_server
30306 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
30307 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
30308 let result_id = Some(new_result_id.to_string());
30309 assert_eq!(
30310 params.text_document.uri,
30311 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
30312 );
30313 async move {
30314 Ok(lsp::DocumentDiagnosticReportResult::Report(
30315 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
30316 related_documents: None,
30317 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
30318 items: Vec::new(),
30319 result_id,
30320 },
30321 }),
30322 ))
30323 }
30324 });
30325
30326 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
30327 project.update(cx, |project, cx| {
30328 let buffer_id = editor
30329 .read(cx)
30330 .buffer()
30331 .read(cx)
30332 .as_singleton()
30333 .expect("created a singleton buffer")
30334 .read(cx)
30335 .remote_id();
30336 let buffer_result_id = project
30337 .lsp_store()
30338 .read(cx)
30339 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
30340 assert_eq!(expected_result_id, buffer_result_id);
30341 });
30342 };
30343
30344 ensure_result_id(None, cx);
30345 cx.executor().advance_clock(Duration::from_millis(60));
30346 cx.executor().run_until_parked();
30347 assert_eq!(
30348 diagnostic_requests.load(atomic::Ordering::Acquire),
30349 1,
30350 "Opening file should trigger diagnostic request"
30351 );
30352 first_request
30353 .next()
30354 .await
30355 .expect("should have sent the first diagnostics pull request");
30356 ensure_result_id(Some(SharedString::new_static("1")), cx);
30357
30358 // Editing should trigger diagnostics
30359 editor.update_in(cx, |editor, window, cx| {
30360 editor.handle_input("2", window, cx)
30361 });
30362 cx.executor().advance_clock(Duration::from_millis(60));
30363 cx.executor().run_until_parked();
30364 assert_eq!(
30365 diagnostic_requests.load(atomic::Ordering::Acquire),
30366 2,
30367 "Editing should trigger diagnostic request"
30368 );
30369 ensure_result_id(Some(SharedString::new_static("2")), cx);
30370
30371 // Moving cursor should not trigger diagnostic request
30372 editor.update_in(cx, |editor, window, cx| {
30373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30374 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
30375 });
30376 });
30377 cx.executor().advance_clock(Duration::from_millis(60));
30378 cx.executor().run_until_parked();
30379 assert_eq!(
30380 diagnostic_requests.load(atomic::Ordering::Acquire),
30381 2,
30382 "Cursor movement should not trigger diagnostic request"
30383 );
30384 ensure_result_id(Some(SharedString::new_static("2")), cx);
30385 // Multiple rapid edits should be debounced
30386 for _ in 0..5 {
30387 editor.update_in(cx, |editor, window, cx| {
30388 editor.handle_input("x", window, cx)
30389 });
30390 }
30391 cx.executor().advance_clock(Duration::from_millis(60));
30392 cx.executor().run_until_parked();
30393
30394 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
30395 assert!(
30396 final_requests <= 4,
30397 "Multiple rapid edits should be debounced (got {final_requests} requests)",
30398 );
30399 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
30400}
30401
30402#[gpui::test]
30403async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
30404 // Regression test for issue #11671
30405 // Previously, adding a cursor after moving multiple cursors would reset
30406 // the cursor count instead of adding to the existing cursors.
30407 init_test(cx, |_| {});
30408 let mut cx = EditorTestContext::new(cx).await;
30409
30410 // Create a simple buffer with cursor at start
30411 cx.set_state(indoc! {"
30412 ˇaaaa
30413 bbbb
30414 cccc
30415 dddd
30416 eeee
30417 ffff
30418 gggg
30419 hhhh"});
30420
30421 // Add 2 cursors below (so we have 3 total)
30422 cx.update_editor(|editor, window, cx| {
30423 editor.add_selection_below(&Default::default(), window, cx);
30424 editor.add_selection_below(&Default::default(), window, cx);
30425 });
30426
30427 // Verify we have 3 cursors
30428 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
30429 assert_eq!(
30430 initial_count, 3,
30431 "Should have 3 cursors after adding 2 below"
30432 );
30433
30434 // Move down one line
30435 cx.update_editor(|editor, window, cx| {
30436 editor.move_down(&MoveDown, window, cx);
30437 });
30438
30439 // Add another cursor below
30440 cx.update_editor(|editor, window, cx| {
30441 editor.add_selection_below(&Default::default(), window, cx);
30442 });
30443
30444 // Should now have 4 cursors (3 original + 1 new)
30445 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
30446 assert_eq!(
30447 final_count, 4,
30448 "Should have 4 cursors after moving and adding another"
30449 );
30450}
30451
30452#[gpui::test]
30453async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
30454 init_test(cx, |_| {});
30455
30456 let mut cx = EditorTestContext::new(cx).await;
30457
30458 cx.set_state(indoc!(
30459 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
30460 Second line here"#
30461 ));
30462
30463 cx.update_editor(|editor, window, cx| {
30464 // Enable soft wrapping with a narrow width to force soft wrapping and
30465 // confirm that more than 2 rows are being displayed.
30466 editor.set_wrap_width(Some(100.0.into()), cx);
30467 assert!(editor.display_text(cx).lines().count() > 2);
30468
30469 editor.add_selection_below(
30470 &AddSelectionBelow {
30471 skip_soft_wrap: true,
30472 },
30473 window,
30474 cx,
30475 );
30476
30477 assert_eq!(
30478 display_ranges(editor, cx),
30479 &[
30480 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
30481 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
30482 ]
30483 );
30484
30485 editor.add_selection_above(
30486 &AddSelectionAbove {
30487 skip_soft_wrap: true,
30488 },
30489 window,
30490 cx,
30491 );
30492
30493 assert_eq!(
30494 display_ranges(editor, cx),
30495 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
30496 );
30497
30498 editor.add_selection_below(
30499 &AddSelectionBelow {
30500 skip_soft_wrap: false,
30501 },
30502 window,
30503 cx,
30504 );
30505
30506 assert_eq!(
30507 display_ranges(editor, cx),
30508 &[
30509 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
30510 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
30511 ]
30512 );
30513
30514 editor.add_selection_above(
30515 &AddSelectionAbove {
30516 skip_soft_wrap: false,
30517 },
30518 window,
30519 cx,
30520 );
30521
30522 assert_eq!(
30523 display_ranges(editor, cx),
30524 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
30525 );
30526 });
30527
30528 // Set up text where selections are in the middle of a soft-wrapped line.
30529 // When adding selection below with `skip_soft_wrap` set to `true`, the new
30530 // selection should be at the same buffer column, not the same pixel
30531 // position.
30532 cx.set_state(indoc!(
30533 r#"1. Very long line to show «howˇ» a wrapped line would look
30534 2. Very long line to show how a wrapped line would look"#
30535 ));
30536
30537 cx.update_editor(|editor, window, cx| {
30538 // Enable soft wrapping with a narrow width to force soft wrapping and
30539 // confirm that more than 2 rows are being displayed.
30540 editor.set_wrap_width(Some(100.0.into()), cx);
30541 assert!(editor.display_text(cx).lines().count() > 2);
30542
30543 editor.add_selection_below(
30544 &AddSelectionBelow {
30545 skip_soft_wrap: true,
30546 },
30547 window,
30548 cx,
30549 );
30550
30551 // Assert that there's now 2 selections, both selecting the same column
30552 // range in the buffer row.
30553 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
30554 let selections = editor.selections.all::<Point>(&display_map);
30555 assert_eq!(selections.len(), 2);
30556 assert_eq!(selections[0].start.column, selections[1].start.column);
30557 assert_eq!(selections[0].end.column, selections[1].end.column);
30558 });
30559}
30560
30561#[gpui::test]
30562async fn test_insert_snippet(cx: &mut TestAppContext) {
30563 init_test(cx, |_| {});
30564 let mut cx = EditorTestContext::new(cx).await;
30565
30566 cx.update_editor(|editor, _, cx| {
30567 editor.project().unwrap().update(cx, |project, cx| {
30568 project.snippets().update(cx, |snippets, _cx| {
30569 let snippet = project::snippet_provider::Snippet {
30570 prefix: vec![], // no prefix needed!
30571 body: "an Unspecified".to_string(),
30572 description: Some("shhhh it's a secret".to_string()),
30573 name: "super secret snippet".to_string(),
30574 };
30575 snippets.add_snippet_for_test(
30576 None,
30577 PathBuf::from("test_snippets.json"),
30578 vec![Arc::new(snippet)],
30579 );
30580
30581 let snippet = project::snippet_provider::Snippet {
30582 prefix: vec![], // no prefix needed!
30583 body: " Location".to_string(),
30584 description: Some("the word 'location'".to_string()),
30585 name: "location word".to_string(),
30586 };
30587 snippets.add_snippet_for_test(
30588 Some("Markdown".to_string()),
30589 PathBuf::from("test_snippets.json"),
30590 vec![Arc::new(snippet)],
30591 );
30592 });
30593 })
30594 });
30595
30596 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
30597
30598 cx.update_editor(|editor, window, cx| {
30599 editor.insert_snippet_at_selections(
30600 &InsertSnippet {
30601 language: None,
30602 name: Some("super secret snippet".to_string()),
30603 snippet: None,
30604 },
30605 window,
30606 cx,
30607 );
30608
30609 // Language is specified in the action,
30610 // so the buffer language does not need to match
30611 editor.insert_snippet_at_selections(
30612 &InsertSnippet {
30613 language: Some("Markdown".to_string()),
30614 name: Some("location word".to_string()),
30615 snippet: None,
30616 },
30617 window,
30618 cx,
30619 );
30620
30621 editor.insert_snippet_at_selections(
30622 &InsertSnippet {
30623 language: None,
30624 name: None,
30625 snippet: Some("$0 after".to_string()),
30626 },
30627 window,
30628 cx,
30629 );
30630 });
30631
30632 cx.assert_editor_state(
30633 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
30634 );
30635}
30636
30637#[gpui::test]
30638async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
30639 use crate::inlays::inlay_hints::InlayHintRefreshReason;
30640 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
30641 use settings::InlayHintSettingsContent;
30642 use std::sync::atomic::AtomicU32;
30643 use std::time::Duration;
30644
30645 const BASE_TIMEOUT_SECS: u64 = 1;
30646
30647 let request_count = Arc::new(AtomicU32::new(0));
30648 let closure_request_count = request_count.clone();
30649
30650 init_test(cx, &|settings| {
30651 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
30652 enabled: Some(true),
30653 ..InlayHintSettingsContent::default()
30654 })
30655 });
30656 cx.update(|cx| {
30657 SettingsStore::update_global(cx, |store, cx| {
30658 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
30659 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
30660 request_timeout: Some(BASE_TIMEOUT_SECS),
30661 button: Some(true),
30662 notifications: None,
30663 semantic_token_rules: None,
30664 });
30665 });
30666 });
30667 });
30668
30669 let fs = FakeFs::new(cx.executor());
30670 fs.insert_tree(
30671 path!("/a"),
30672 json!({
30673 "main.rs": "fn main() { let a = 5; }",
30674 }),
30675 )
30676 .await;
30677
30678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
30679 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30680 language_registry.add(rust_lang());
30681 let mut fake_servers = language_registry.register_fake_lsp(
30682 "Rust",
30683 FakeLspAdapter {
30684 capabilities: lsp::ServerCapabilities {
30685 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30686 ..lsp::ServerCapabilities::default()
30687 },
30688 initializer: Some(Box::new(move |fake_server| {
30689 let request_count = closure_request_count.clone();
30690 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30691 move |params, cx| {
30692 let request_count = request_count.clone();
30693 async move {
30694 cx.background_executor()
30695 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
30696 .await;
30697 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
30698 assert_eq!(
30699 params.text_document.uri,
30700 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
30701 );
30702 Ok(Some(vec![lsp::InlayHint {
30703 position: lsp::Position::new(0, 1),
30704 label: lsp::InlayHintLabel::String(count.to_string()),
30705 kind: None,
30706 text_edits: None,
30707 tooltip: None,
30708 padding_left: None,
30709 padding_right: None,
30710 data: None,
30711 }]))
30712 }
30713 },
30714 );
30715 })),
30716 ..FakeLspAdapter::default()
30717 },
30718 );
30719
30720 let buffer = project
30721 .update(cx, |project, cx| {
30722 project.open_local_buffer(path!("/a/main.rs"), cx)
30723 })
30724 .await
30725 .unwrap();
30726 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
30727
30728 cx.executor().run_until_parked();
30729 let fake_server = fake_servers.next().await.unwrap();
30730
30731 cx.executor()
30732 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
30733 cx.executor().run_until_parked();
30734 editor
30735 .update(cx, |editor, _window, cx| {
30736 assert!(
30737 cached_hint_labels(editor, cx).is_empty(),
30738 "First request should time out, no hints cached"
30739 );
30740 })
30741 .unwrap();
30742
30743 editor
30744 .update(cx, |editor, _window, cx| {
30745 editor.refresh_inlay_hints(
30746 InlayHintRefreshReason::RefreshRequested {
30747 server_id: fake_server.server.server_id(),
30748 request_id: Some(1),
30749 },
30750 cx,
30751 );
30752 })
30753 .unwrap();
30754 cx.executor()
30755 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
30756 cx.executor().run_until_parked();
30757 editor
30758 .update(cx, |editor, _window, cx| {
30759 assert!(
30760 cached_hint_labels(editor, cx).is_empty(),
30761 "Second request should also time out with BASE_TIMEOUT, no hints cached"
30762 );
30763 })
30764 .unwrap();
30765
30766 cx.update(|cx| {
30767 SettingsStore::update_global(cx, |store, cx| {
30768 store.update_user_settings(cx, |settings| {
30769 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
30770 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
30771 button: Some(true),
30772 notifications: None,
30773 semantic_token_rules: None,
30774 });
30775 });
30776 });
30777 });
30778 editor
30779 .update(cx, |editor, _window, cx| {
30780 editor.refresh_inlay_hints(
30781 InlayHintRefreshReason::RefreshRequested {
30782 server_id: fake_server.server.server_id(),
30783 request_id: Some(2),
30784 },
30785 cx,
30786 );
30787 })
30788 .unwrap();
30789 cx.executor()
30790 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
30791 cx.executor().run_until_parked();
30792 editor
30793 .update(cx, |editor, _window, cx| {
30794 assert_eq!(
30795 vec!["1".to_string()],
30796 cached_hint_labels(editor, cx),
30797 "With extended timeout (BASE * 4), hints should arrive successfully"
30798 );
30799 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
30800 })
30801 .unwrap();
30802}
30803
30804#[gpui::test]
30805async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
30806 init_test(cx, |_| {});
30807 let (editor, cx) = cx.add_window_view(Editor::single_line);
30808 editor.update_in(cx, |editor, window, cx| {
30809 editor.set_text("oops\n\nwow\n", window, cx)
30810 });
30811 cx.run_until_parked();
30812 editor.update(cx, |editor, cx| {
30813 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
30814 });
30815 editor.update(cx, |editor, cx| {
30816 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
30817 });
30818 cx.run_until_parked();
30819 editor.update(cx, |editor, cx| {
30820 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
30821 });
30822}
30823
30824#[gpui::test]
30825async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
30826 init_test(cx, |_| {});
30827
30828 cx.update(|cx| {
30829 register_project_item::<Editor>(cx);
30830 });
30831
30832 let fs = FakeFs::new(cx.executor());
30833 fs.insert_tree("/root1", json!({})).await;
30834 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
30835 .await;
30836
30837 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
30838 let (multi_workspace, cx) =
30839 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30840 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
30841
30842 let worktree_id = project.update(cx, |project, cx| {
30843 project.worktrees(cx).next().unwrap().read(cx).id()
30844 });
30845
30846 let handle = workspace
30847 .update_in(cx, |workspace, window, cx| {
30848 let project_path = (worktree_id, rel_path("one.pdf"));
30849 workspace.open_path(project_path, None, true, window, cx)
30850 })
30851 .await
30852 .unwrap();
30853 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
30854 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
30855 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
30856 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
30857}
30858
30859#[gpui::test]
30860async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
30861 init_test(cx, |_| {});
30862
30863 let language = Arc::new(Language::new(
30864 LanguageConfig::default(),
30865 Some(tree_sitter_rust::LANGUAGE.into()),
30866 ));
30867
30868 // Test hierarchical sibling navigation
30869 let text = r#"
30870 fn outer() {
30871 if condition {
30872 let a = 1;
30873 }
30874 let b = 2;
30875 }
30876
30877 fn another() {
30878 let c = 3;
30879 }
30880 "#;
30881
30882 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
30883 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30884 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
30885
30886 // Wait for parsing to complete
30887 editor
30888 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
30889 .await;
30890
30891 editor.update_in(cx, |editor, window, cx| {
30892 // Start by selecting "let a = 1;" inside the if block
30893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30894 s.select_display_ranges([
30895 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
30896 ]);
30897 });
30898
30899 let initial_selection = editor
30900 .selections
30901 .display_ranges(&editor.display_snapshot(cx));
30902 assert_eq!(initial_selection.len(), 1, "Should have one selection");
30903
30904 // Test select next sibling - should move up levels to find the next sibling
30905 // Since "let a = 1;" has no siblings in the if block, it should move up
30906 // to find "let b = 2;" which is a sibling of the if block
30907 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
30908 let next_selection = editor
30909 .selections
30910 .display_ranges(&editor.display_snapshot(cx));
30911
30912 // Should have a selection and it should be different from the initial
30913 assert_eq!(
30914 next_selection.len(),
30915 1,
30916 "Should have one selection after next"
30917 );
30918 assert_ne!(
30919 next_selection[0], initial_selection[0],
30920 "Next sibling selection should be different"
30921 );
30922
30923 // Test hierarchical navigation by going to the end of the current function
30924 // and trying to navigate to the next function
30925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30926 s.select_display_ranges([
30927 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
30928 ]);
30929 });
30930
30931 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
30932 let function_next_selection = editor
30933 .selections
30934 .display_ranges(&editor.display_snapshot(cx));
30935
30936 // Should move to the next function
30937 assert_eq!(
30938 function_next_selection.len(),
30939 1,
30940 "Should have one selection after function next"
30941 );
30942
30943 // Test select previous sibling navigation
30944 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
30945 let prev_selection = editor
30946 .selections
30947 .display_ranges(&editor.display_snapshot(cx));
30948
30949 // Should have a selection and it should be different
30950 assert_eq!(
30951 prev_selection.len(),
30952 1,
30953 "Should have one selection after prev"
30954 );
30955 assert_ne!(
30956 prev_selection[0], function_next_selection[0],
30957 "Previous sibling selection should be different from next"
30958 );
30959 });
30960}
30961
30962#[gpui::test]
30963async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
30964 init_test(cx, |_| {});
30965
30966 let mut cx = EditorTestContext::new(cx).await;
30967 cx.set_state(
30968 "let ˇvariable = 42;
30969let another = variable + 1;
30970let result = variable * 2;",
30971 );
30972
30973 // Set up document highlights manually (simulating LSP response)
30974 cx.update_editor(|editor, _window, cx| {
30975 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
30976
30977 // Create highlights for "variable" occurrences
30978 let highlight_ranges = [
30979 Point::new(0, 4)..Point::new(0, 12), // First "variable"
30980 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
30981 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
30982 ];
30983
30984 let anchor_ranges: Vec<_> = highlight_ranges
30985 .iter()
30986 .map(|range| range.clone().to_anchors(&buffer_snapshot))
30987 .collect();
30988
30989 editor.highlight_background(
30990 HighlightKey::DocumentHighlightRead,
30991 &anchor_ranges,
30992 |_, theme| theme.colors().editor_document_highlight_read_background,
30993 cx,
30994 );
30995 });
30996
30997 // Go to next highlight - should move to second "variable"
30998 cx.update_editor(|editor, window, cx| {
30999 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
31000 });
31001 cx.assert_editor_state(
31002 "let variable = 42;
31003let another = ˇvariable + 1;
31004let result = variable * 2;",
31005 );
31006
31007 // Go to next highlight - should move to third "variable"
31008 cx.update_editor(|editor, window, cx| {
31009 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
31010 });
31011 cx.assert_editor_state(
31012 "let variable = 42;
31013let another = variable + 1;
31014let result = ˇvariable * 2;",
31015 );
31016
31017 // Go to next highlight - should stay at third "variable" (no wrap-around)
31018 cx.update_editor(|editor, window, cx| {
31019 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
31020 });
31021 cx.assert_editor_state(
31022 "let variable = 42;
31023let another = variable + 1;
31024let result = ˇvariable * 2;",
31025 );
31026
31027 // Now test going backwards from third position
31028 cx.update_editor(|editor, window, cx| {
31029 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31030 });
31031 cx.assert_editor_state(
31032 "let variable = 42;
31033let another = ˇvariable + 1;
31034let result = variable * 2;",
31035 );
31036
31037 // Go to previous highlight - should move to first "variable"
31038 cx.update_editor(|editor, window, cx| {
31039 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31040 });
31041 cx.assert_editor_state(
31042 "let ˇvariable = 42;
31043let another = variable + 1;
31044let result = variable * 2;",
31045 );
31046
31047 // Go to previous highlight - should stay on first "variable"
31048 cx.update_editor(|editor, window, cx| {
31049 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31050 });
31051 cx.assert_editor_state(
31052 "let ˇvariable = 42;
31053let another = variable + 1;
31054let result = variable * 2;",
31055 );
31056}
31057
31058#[gpui::test]
31059async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
31060 cx: &mut gpui::TestAppContext,
31061) {
31062 init_test(cx, |_| {});
31063
31064 let url = "https://zed.dev";
31065
31066 let markdown_language = Arc::new(Language::new(
31067 LanguageConfig {
31068 name: "Markdown".into(),
31069 ..LanguageConfig::default()
31070 },
31071 None,
31072 ));
31073
31074 let mut cx = EditorTestContext::new(cx).await;
31075 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31076 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
31077
31078 cx.update_editor(|editor, window, cx| {
31079 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31080 editor.paste(&Paste, window, cx);
31081 });
31082
31083 cx.assert_editor_state(&format!(
31084 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
31085 ));
31086}
31087
31088#[gpui::test]
31089async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
31090 init_test(cx, |_| {});
31091
31092 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31093 let mut cx = EditorTestContext::new(cx).await;
31094
31095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31096
31097 // Case 1: Test if adding a character with multi cursors preserves nested list indents
31098 cx.set_state(&indoc! {"
31099 - [ ] Item 1
31100 - [ ] Item 1.a
31101 - [ˇ] Item 2
31102 - [ˇ] Item 2.a
31103 - [ˇ] Item 2.b
31104 "
31105 });
31106 cx.update_editor(|editor, window, cx| {
31107 editor.handle_input("x", window, cx);
31108 });
31109 cx.run_until_parked();
31110 cx.assert_editor_state(indoc! {"
31111 - [ ] Item 1
31112 - [ ] Item 1.a
31113 - [xˇ] Item 2
31114 - [xˇ] Item 2.a
31115 - [xˇ] Item 2.b
31116 "
31117 });
31118
31119 // Case 2: Test adding new line after nested list continues the list with unchecked task
31120 cx.set_state(&indoc! {"
31121 - [ ] Item 1
31122 - [ ] Item 1.a
31123 - [x] Item 2
31124 - [x] Item 2.a
31125 - [x] Item 2.bˇ"
31126 });
31127 cx.update_editor(|editor, window, cx| {
31128 editor.newline(&Newline, window, cx);
31129 });
31130 cx.assert_editor_state(indoc! {"
31131 - [ ] Item 1
31132 - [ ] Item 1.a
31133 - [x] Item 2
31134 - [x] Item 2.a
31135 - [x] Item 2.b
31136 - [ ] ˇ"
31137 });
31138
31139 // Case 3: Test adding content to continued list item
31140 cx.update_editor(|editor, window, cx| {
31141 editor.handle_input("Item 2.c", window, cx);
31142 });
31143 cx.run_until_parked();
31144 cx.assert_editor_state(indoc! {"
31145 - [ ] Item 1
31146 - [ ] Item 1.a
31147 - [x] Item 2
31148 - [x] Item 2.a
31149 - [x] Item 2.b
31150 - [ ] Item 2.cˇ"
31151 });
31152
31153 // Case 4: Test adding new line after nested ordered list continues with next number
31154 cx.set_state(indoc! {"
31155 1. Item 1
31156 1. Item 1.a
31157 2. Item 2
31158 1. Item 2.a
31159 2. Item 2.bˇ"
31160 });
31161 cx.update_editor(|editor, window, cx| {
31162 editor.newline(&Newline, window, cx);
31163 });
31164 cx.assert_editor_state(indoc! {"
31165 1. Item 1
31166 1. Item 1.a
31167 2. Item 2
31168 1. Item 2.a
31169 2. Item 2.b
31170 3. ˇ"
31171 });
31172
31173 // Case 5: Adding content to continued ordered list item
31174 cx.update_editor(|editor, window, cx| {
31175 editor.handle_input("Item 2.c", window, cx);
31176 });
31177 cx.run_until_parked();
31178 cx.assert_editor_state(indoc! {"
31179 1. Item 1
31180 1. Item 1.a
31181 2. Item 2
31182 1. Item 2.a
31183 2. Item 2.b
31184 3. Item 2.cˇ"
31185 });
31186
31187 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
31188 cx.set_state(indoc! {"
31189 - Item 1
31190 - Item 1.a
31191 - Item 1.a
31192 ˇ"});
31193 cx.update_editor(|editor, window, cx| {
31194 editor.handle_input("-", window, cx);
31195 });
31196 cx.run_until_parked();
31197 cx.assert_editor_state(indoc! {"
31198 - Item 1
31199 - Item 1.a
31200 - Item 1.a
31201 -ˇ"});
31202
31203 // Case 7: Test blockquote newline preserves something
31204 cx.set_state(indoc! {"
31205 > Item 1ˇ"
31206 });
31207 cx.update_editor(|editor, window, cx| {
31208 editor.newline(&Newline, window, cx);
31209 });
31210 cx.assert_editor_state(indoc! {"
31211 > Item 1
31212 ˇ"
31213 });
31214}
31215
31216#[gpui::test]
31217async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
31218 cx: &mut gpui::TestAppContext,
31219) {
31220 init_test(cx, |_| {});
31221
31222 let url = "https://zed.dev";
31223
31224 let markdown_language = Arc::new(Language::new(
31225 LanguageConfig {
31226 name: "Markdown".into(),
31227 ..LanguageConfig::default()
31228 },
31229 None,
31230 ));
31231
31232 let mut cx = EditorTestContext::new(cx).await;
31233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31234 cx.set_state(&format!(
31235 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
31236 ));
31237
31238 cx.update_editor(|editor, window, cx| {
31239 editor.copy(&Copy, window, cx);
31240 });
31241
31242 cx.set_state(&format!(
31243 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
31244 ));
31245
31246 cx.update_editor(|editor, window, cx| {
31247 editor.paste(&Paste, window, cx);
31248 });
31249
31250 cx.assert_editor_state(&format!(
31251 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
31252 ));
31253}
31254
31255#[gpui::test]
31256async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
31257 cx: &mut gpui::TestAppContext,
31258) {
31259 init_test(cx, |_| {});
31260
31261 let url = "https://zed.dev";
31262
31263 let markdown_language = Arc::new(Language::new(
31264 LanguageConfig {
31265 name: "Markdown".into(),
31266 ..LanguageConfig::default()
31267 },
31268 None,
31269 ));
31270
31271 let mut cx = EditorTestContext::new(cx).await;
31272 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31273 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
31274
31275 cx.update_editor(|editor, window, cx| {
31276 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31277 editor.paste(&Paste, window, cx);
31278 });
31279
31280 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
31281}
31282
31283#[gpui::test]
31284async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
31285 cx: &mut gpui::TestAppContext,
31286) {
31287 init_test(cx, |_| {});
31288
31289 let text = "Awesome";
31290
31291 let markdown_language = Arc::new(Language::new(
31292 LanguageConfig {
31293 name: "Markdown".into(),
31294 ..LanguageConfig::default()
31295 },
31296 None,
31297 ));
31298
31299 let mut cx = EditorTestContext::new(cx).await;
31300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31301 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
31302
31303 cx.update_editor(|editor, window, cx| {
31304 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
31305 editor.paste(&Paste, window, cx);
31306 });
31307
31308 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
31309}
31310
31311#[gpui::test]
31312async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
31313 cx: &mut gpui::TestAppContext,
31314) {
31315 init_test(cx, |_| {});
31316
31317 let url = "https://zed.dev";
31318
31319 let markdown_language = Arc::new(Language::new(
31320 LanguageConfig {
31321 name: "Rust".into(),
31322 ..LanguageConfig::default()
31323 },
31324 None,
31325 ));
31326
31327 let mut cx = EditorTestContext::new(cx).await;
31328 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31329 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
31330
31331 cx.update_editor(|editor, window, cx| {
31332 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31333 editor.paste(&Paste, window, cx);
31334 });
31335
31336 cx.assert_editor_state(&format!(
31337 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
31338 ));
31339}
31340
31341#[gpui::test]
31342async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
31343 cx: &mut TestAppContext,
31344) {
31345 init_test(cx, |_| {});
31346
31347 let url = "https://zed.dev";
31348
31349 let markdown_language = Arc::new(Language::new(
31350 LanguageConfig {
31351 name: "Markdown".into(),
31352 ..LanguageConfig::default()
31353 },
31354 None,
31355 ));
31356
31357 let (editor, cx) = cx.add_window_view(|window, cx| {
31358 let multi_buffer = MultiBuffer::build_multi(
31359 [
31360 ("this will embed -> link", vec![Point::row_range(0..1)]),
31361 ("this will replace -> link", vec![Point::row_range(0..1)]),
31362 ],
31363 cx,
31364 );
31365 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
31366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31367 s.select_ranges(vec![
31368 Point::new(0, 19)..Point::new(0, 23),
31369 Point::new(1, 21)..Point::new(1, 25),
31370 ])
31371 });
31372 let snapshot = multi_buffer.read(cx).snapshot(cx);
31373 let first_buffer_id = snapshot.all_buffer_ids().next().unwrap();
31374 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
31375 first_buffer.update(cx, |buffer, cx| {
31376 buffer.set_language(Some(markdown_language.clone()), cx);
31377 });
31378
31379 editor
31380 });
31381 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
31382
31383 cx.update_editor(|editor, window, cx| {
31384 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31385 editor.paste(&Paste, window, cx);
31386 });
31387
31388 cx.assert_editor_state(&format!(
31389 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
31390 ));
31391}
31392
31393#[gpui::test]
31394async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
31395 init_test(cx, |_| {});
31396
31397 let fs = FakeFs::new(cx.executor());
31398 fs.insert_tree(
31399 path!("/project"),
31400 json!({
31401 "first.rs": "# First Document\nSome content here.",
31402 "second.rs": "Plain text content for second file.",
31403 }),
31404 )
31405 .await;
31406
31407 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
31408 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31409 let cx = &mut VisualTestContext::from_window(*window, cx);
31410
31411 let language = rust_lang();
31412 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
31413 language_registry.add(language.clone());
31414 let mut fake_servers = language_registry.register_fake_lsp(
31415 "Rust",
31416 FakeLspAdapter {
31417 ..FakeLspAdapter::default()
31418 },
31419 );
31420
31421 let buffer1 = project
31422 .update(cx, |project, cx| {
31423 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
31424 })
31425 .await
31426 .unwrap();
31427 let buffer2 = project
31428 .update(cx, |project, cx| {
31429 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
31430 })
31431 .await
31432 .unwrap();
31433
31434 let multi_buffer = cx.new(|cx| {
31435 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
31436 multi_buffer.set_excerpts_for_path(
31437 PathKey::for_buffer(&buffer1, cx),
31438 buffer1.clone(),
31439 [Point::zero()..buffer1.read(cx).max_point()],
31440 3,
31441 cx,
31442 );
31443 multi_buffer.set_excerpts_for_path(
31444 PathKey::for_buffer(&buffer2, cx),
31445 buffer2.clone(),
31446 [Point::zero()..buffer1.read(cx).max_point()],
31447 3,
31448 cx,
31449 );
31450 multi_buffer
31451 });
31452
31453 let (editor, cx) = cx.add_window_view(|window, cx| {
31454 Editor::new(
31455 EditorMode::full(),
31456 multi_buffer,
31457 Some(project.clone()),
31458 window,
31459 cx,
31460 )
31461 });
31462
31463 let fake_language_server = fake_servers.next().await.unwrap();
31464
31465 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
31466
31467 let save = editor.update_in(cx, |editor, window, cx| {
31468 assert!(editor.is_dirty(cx));
31469
31470 editor.save(
31471 SaveOptions {
31472 format: true,
31473 autosave: true,
31474 },
31475 project,
31476 window,
31477 cx,
31478 )
31479 });
31480 let (start_edit_tx, start_edit_rx) = oneshot::channel();
31481 let (done_edit_tx, done_edit_rx) = oneshot::channel();
31482 let mut done_edit_rx = Some(done_edit_rx);
31483 let mut start_edit_tx = Some(start_edit_tx);
31484
31485 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
31486 start_edit_tx.take().unwrap().send(()).unwrap();
31487 let done_edit_rx = done_edit_rx.take().unwrap();
31488 async move {
31489 done_edit_rx.await.unwrap();
31490 Ok(None)
31491 }
31492 });
31493
31494 start_edit_rx.await.unwrap();
31495 buffer2
31496 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
31497 .unwrap();
31498
31499 done_edit_tx.send(()).unwrap();
31500
31501 save.await.unwrap();
31502 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
31503}
31504
31505#[gpui::test]
31506fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
31507 init_test(cx, |_| {});
31508
31509 let editor = cx.add_window(|window, cx| {
31510 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
31511 build_editor(buffer, window, cx)
31512 });
31513
31514 editor
31515 .update(cx, |editor, window, cx| {
31516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31517 s.select_display_ranges([
31518 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
31519 ])
31520 });
31521
31522 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
31523
31524 assert_eq!(
31525 editor.display_text(cx),
31526 "line1\nline2\nline2",
31527 "Duplicating last line upward should create duplicate above, not on same line"
31528 );
31529
31530 assert_eq!(
31531 editor
31532 .selections
31533 .display_ranges(&editor.display_snapshot(cx)),
31534 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
31535 "Selection should move to the duplicated line"
31536 );
31537 })
31538 .unwrap();
31539}
31540
31541#[gpui::test]
31542async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
31543 init_test(cx, |_| {});
31544
31545 let mut cx = EditorTestContext::new(cx).await;
31546
31547 cx.set_state("line1\nline2ˇ");
31548
31549 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
31550
31551 let clipboard_text = cx
31552 .read_from_clipboard()
31553 .and_then(|item| item.text().as_deref().map(str::to_string));
31554
31555 assert_eq!(
31556 clipboard_text,
31557 Some("line2\n".to_string()),
31558 "Copying a line without trailing newline should include a newline"
31559 );
31560
31561 cx.set_state("line1\nˇ");
31562
31563 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31564
31565 cx.assert_editor_state("line1\nline2\nˇ");
31566}
31567
31568#[gpui::test]
31569async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
31570 init_test(cx, |_| {});
31571
31572 let mut cx = EditorTestContext::new(cx).await;
31573
31574 cx.set_state("ˇline1\nˇline2\nˇline3\n");
31575
31576 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
31577
31578 let clipboard_text = cx
31579 .read_from_clipboard()
31580 .and_then(|item| item.text().as_deref().map(str::to_string));
31581
31582 assert_eq!(
31583 clipboard_text,
31584 Some("line1\nline2\nline3\n".to_string()),
31585 "Copying multiple lines should include a single newline between lines"
31586 );
31587
31588 cx.set_state("lineA\nˇ");
31589
31590 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31591
31592 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
31593}
31594
31595#[gpui::test]
31596async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
31597 init_test(cx, |_| {});
31598
31599 let mut cx = EditorTestContext::new(cx).await;
31600
31601 cx.set_state("ˇline1\nˇline2\nˇline3\n");
31602
31603 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
31604
31605 let clipboard_text = cx
31606 .read_from_clipboard()
31607 .and_then(|item| item.text().as_deref().map(str::to_string));
31608
31609 assert_eq!(
31610 clipboard_text,
31611 Some("line1\nline2\nline3\n".to_string()),
31612 "Copying multiple lines should include a single newline between lines"
31613 );
31614
31615 cx.set_state("lineA\nˇ");
31616
31617 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31618
31619 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
31620}
31621
31622#[gpui::test]
31623async fn test_end_of_editor_context(cx: &mut TestAppContext) {
31624 init_test(cx, |_| {});
31625
31626 let mut cx = EditorTestContext::new(cx).await;
31627
31628 cx.set_state("line1\nline2ˇ");
31629 cx.update_editor(|e, window, cx| {
31630 e.set_mode(EditorMode::SingleLine);
31631 assert!(!e.key_context(window, cx).contains("start_of_input"));
31632 assert!(e.key_context(window, cx).contains("end_of_input"));
31633 });
31634 cx.set_state("ˇline1\nline2");
31635 cx.update_editor(|e, window, cx| {
31636 e.set_mode(EditorMode::SingleLine);
31637 assert!(e.key_context(window, cx).contains("start_of_input"));
31638 assert!(!e.key_context(window, cx).contains("end_of_input"));
31639 });
31640 cx.set_state("line1ˇ\nline2");
31641 cx.update_editor(|e, window, cx| {
31642 e.set_mode(EditorMode::SingleLine);
31643 assert!(!e.key_context(window, cx).contains("start_of_input"));
31644 assert!(!e.key_context(window, cx).contains("end_of_input"));
31645 });
31646
31647 cx.set_state("line1\nline2ˇ");
31648 cx.update_editor(|e, window, cx| {
31649 e.set_mode(EditorMode::AutoHeight {
31650 min_lines: 1,
31651 max_lines: Some(4),
31652 });
31653 assert!(!e.key_context(window, cx).contains("start_of_input"));
31654 assert!(e.key_context(window, cx).contains("end_of_input"));
31655 });
31656 cx.set_state("ˇline1\nline2");
31657 cx.update_editor(|e, window, cx| {
31658 e.set_mode(EditorMode::AutoHeight {
31659 min_lines: 1,
31660 max_lines: Some(4),
31661 });
31662 assert!(e.key_context(window, cx).contains("start_of_input"));
31663 assert!(!e.key_context(window, cx).contains("end_of_input"));
31664 });
31665 cx.set_state("line1ˇ\nline2");
31666 cx.update_editor(|e, window, cx| {
31667 e.set_mode(EditorMode::AutoHeight {
31668 min_lines: 1,
31669 max_lines: Some(4),
31670 });
31671 assert!(!e.key_context(window, cx).contains("start_of_input"));
31672 assert!(!e.key_context(window, cx).contains("end_of_input"));
31673 });
31674}
31675
31676#[gpui::test]
31677async fn test_sticky_scroll(cx: &mut TestAppContext) {
31678 init_test(cx, |_| {});
31679 let mut cx = EditorTestContext::new(cx).await;
31680
31681 let buffer = indoc! {"
31682 ˇfn foo() {
31683 let abc = 123;
31684 }
31685 struct Bar;
31686 impl Bar {
31687 fn new() -> Self {
31688 Self
31689 }
31690 }
31691 fn baz() {
31692 }
31693 "};
31694 cx.set_state(&buffer);
31695
31696 cx.update_editor(|e, _, cx| {
31697 e.buffer()
31698 .read(cx)
31699 .as_singleton()
31700 .unwrap()
31701 .update(cx, |buffer, cx| {
31702 buffer.set_language(Some(rust_lang()), cx);
31703 })
31704 });
31705
31706 let mut sticky_headers = |offset: ScrollOffset| {
31707 cx.update_editor(|e, window, cx| {
31708 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31709 });
31710 cx.run_until_parked();
31711 cx.update_editor(|e, window, cx| {
31712 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31713 .into_iter()
31714 .map(
31715 |StickyHeader {
31716 start_point,
31717 offset,
31718 ..
31719 }| { (start_point, offset) },
31720 )
31721 .collect::<Vec<_>>()
31722 })
31723 };
31724
31725 let fn_foo = Point { row: 0, column: 0 };
31726 let impl_bar = Point { row: 4, column: 0 };
31727 let fn_new = Point { row: 5, column: 4 };
31728
31729 assert_eq!(sticky_headers(0.0), vec![]);
31730 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
31731 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
31732 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
31733 assert_eq!(sticky_headers(2.0), vec![]);
31734 assert_eq!(sticky_headers(2.5), vec![]);
31735 assert_eq!(sticky_headers(3.0), vec![]);
31736 assert_eq!(sticky_headers(3.5), vec![]);
31737 assert_eq!(sticky_headers(4.0), vec![]);
31738 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
31739 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
31740 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
31741 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
31742 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
31743 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
31744 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
31745 assert_eq!(sticky_headers(8.0), vec![]);
31746 assert_eq!(sticky_headers(8.5), vec![]);
31747 assert_eq!(sticky_headers(9.0), vec![]);
31748 assert_eq!(sticky_headers(9.5), vec![]);
31749 assert_eq!(sticky_headers(10.0), vec![]);
31750}
31751
31752#[gpui::test]
31753async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
31754 executor: BackgroundExecutor,
31755 cx: &mut TestAppContext,
31756) {
31757 init_test(cx, |_| {});
31758 let mut cx = EditorTestContext::new(cx).await;
31759
31760 let diff_base = indoc! {"
31761 fn foo() {
31762 let a = 1;
31763 let b = 2;
31764 let c = 3;
31765 let d = 4;
31766 let e = 5;
31767 }
31768 "};
31769
31770 let buffer = indoc! {"
31771 ˇfn foo() {
31772 }
31773 "};
31774
31775 cx.set_state(&buffer);
31776
31777 cx.update_editor(|e, _, cx| {
31778 e.buffer()
31779 .read(cx)
31780 .as_singleton()
31781 .unwrap()
31782 .update(cx, |buffer, cx| {
31783 buffer.set_language(Some(rust_lang()), cx);
31784 })
31785 });
31786
31787 cx.set_head_text(diff_base);
31788 executor.run_until_parked();
31789
31790 cx.update_editor(|editor, window, cx| {
31791 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
31792 });
31793 executor.run_until_parked();
31794
31795 // After expanding, the display should look like:
31796 // row 0: fn foo() {
31797 // row 1: - let a = 1; (deleted)
31798 // row 2: - let b = 2; (deleted)
31799 // row 3: - let c = 3; (deleted)
31800 // row 4: - let d = 4; (deleted)
31801 // row 5: - let e = 5; (deleted)
31802 // row 6: }
31803 //
31804 // fn foo() spans display rows 0-6. Scrolling into the deleted region
31805 // (rows 1-5) should still show fn foo() as a sticky header.
31806
31807 let fn_foo = Point { row: 0, column: 0 };
31808
31809 let mut sticky_headers = |offset: ScrollOffset| {
31810 cx.update_editor(|e, window, cx| {
31811 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31812 });
31813 cx.run_until_parked();
31814 cx.update_editor(|e, window, cx| {
31815 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31816 .into_iter()
31817 .map(
31818 |StickyHeader {
31819 start_point,
31820 offset,
31821 ..
31822 }| { (start_point, offset) },
31823 )
31824 .collect::<Vec<_>>()
31825 })
31826 };
31827
31828 assert_eq!(sticky_headers(0.0), vec![]);
31829 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
31830 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
31831 // Scrolling into deleted lines: fn foo() should still be a sticky header.
31832 assert_eq!(sticky_headers(2.0), vec![(fn_foo, 0.0)]);
31833 assert_eq!(sticky_headers(3.0), vec![(fn_foo, 0.0)]);
31834 assert_eq!(sticky_headers(4.0), vec![(fn_foo, 0.0)]);
31835 assert_eq!(sticky_headers(5.0), vec![(fn_foo, 0.0)]);
31836 assert_eq!(sticky_headers(5.5), vec![(fn_foo, -0.5)]);
31837 // Past the closing brace: no more sticky header.
31838 assert_eq!(sticky_headers(6.0), vec![]);
31839}
31840
31841#[gpui::test]
31842async fn test_no_duplicated_sticky_headers(cx: &mut TestAppContext) {
31843 init_test(cx, |_| {});
31844 let mut cx = EditorTestContext::new(cx).await;
31845
31846 cx.set_state(indoc! {"
31847 ˇimpl Foo { fn bar() {
31848 let x = 1;
31849 fn baz() {
31850 let y = 2;
31851 }
31852 } }
31853 "});
31854
31855 cx.update_editor(|e, _, cx| {
31856 e.buffer()
31857 .read(cx)
31858 .as_singleton()
31859 .unwrap()
31860 .update(cx, |buffer, cx| {
31861 buffer.set_language(Some(rust_lang()), cx);
31862 })
31863 });
31864
31865 let mut sticky_headers = |offset: ScrollOffset| {
31866 cx.update_editor(|e, window, cx| {
31867 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31868 });
31869 cx.run_until_parked();
31870 cx.update_editor(|e, window, cx| {
31871 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31872 .into_iter()
31873 .map(
31874 |StickyHeader {
31875 start_point,
31876 offset,
31877 ..
31878 }| { (start_point, offset) },
31879 )
31880 .collect::<Vec<_>>()
31881 })
31882 };
31883
31884 let struct_foo = Point { row: 0, column: 0 };
31885 let fn_baz = Point { row: 2, column: 4 };
31886
31887 assert_eq!(sticky_headers(0.0), vec![]);
31888 assert_eq!(sticky_headers(0.5), vec![(struct_foo, 0.0)]);
31889 assert_eq!(sticky_headers(1.0), vec![(struct_foo, 0.0)]);
31890 assert_eq!(sticky_headers(1.5), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
31891 assert_eq!(sticky_headers(2.0), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
31892 assert_eq!(sticky_headers(2.5), vec![(struct_foo, 0.0), (fn_baz, 0.5)]);
31893 assert_eq!(sticky_headers(3.0), vec![(struct_foo, 0.0)]);
31894 assert_eq!(sticky_headers(3.5), vec![(struct_foo, 0.0)]);
31895 assert_eq!(sticky_headers(4.0), vec![(struct_foo, 0.0)]);
31896 assert_eq!(sticky_headers(4.5), vec![(struct_foo, -0.5)]);
31897 assert_eq!(sticky_headers(5.0), vec![]);
31898}
31899
31900#[gpui::test]
31901fn test_relative_line_numbers(cx: &mut TestAppContext) {
31902 init_test(cx, |_| {});
31903
31904 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
31905 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
31906 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
31907
31908 let multibuffer = cx.new(|cx| {
31909 let mut multibuffer = MultiBuffer::new(ReadWrite);
31910 multibuffer.set_excerpts_for_path(
31911 PathKey::sorted(0),
31912 buffer_1.clone(),
31913 [Point::new(0, 0)..Point::new(2, 0)],
31914 0,
31915 cx,
31916 );
31917 multibuffer.set_excerpts_for_path(
31918 PathKey::sorted(1),
31919 buffer_2.clone(),
31920 [Point::new(0, 0)..Point::new(2, 0)],
31921 0,
31922 cx,
31923 );
31924 multibuffer.set_excerpts_for_path(
31925 PathKey::sorted(2),
31926 buffer_3.clone(),
31927 [Point::new(0, 0)..Point::new(2, 0)],
31928 0,
31929 cx,
31930 );
31931 multibuffer
31932 });
31933
31934 // wrapped contents of multibuffer:
31935 // aaa
31936 // aaa
31937 // aaa
31938 // a
31939 // bbb
31940 //
31941 // ccc
31942 // ccc
31943 // ccc
31944 // c
31945 // ddd
31946 //
31947 // eee
31948 // fff
31949 // fff
31950 // fff
31951 // f
31952
31953 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
31954 _ = editor.update(cx, |editor, window, cx| {
31955 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
31956
31957 // includes trailing newlines.
31958 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
31959 let expected_wrapped_line_numbers = [
31960 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
31961 ];
31962
31963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31964 s.select_ranges([
31965 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
31966 ]);
31967 });
31968
31969 let snapshot = editor.snapshot(window, cx);
31970
31971 // these are all 0-indexed
31972 let base_display_row = DisplayRow(11);
31973 let base_row = 3;
31974 let wrapped_base_row = 7;
31975
31976 // test not counting wrapped lines
31977 let expected_relative_numbers = expected_line_numbers
31978 .into_iter()
31979 .enumerate()
31980 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
31981 .filter(|(_, relative_line_number)| *relative_line_number != 0)
31982 .collect_vec();
31983 let actual_relative_numbers = snapshot
31984 .calculate_relative_line_numbers(
31985 &(DisplayRow(0)..DisplayRow(24)),
31986 base_display_row,
31987 false,
31988 )
31989 .into_iter()
31990 .sorted()
31991 .collect_vec();
31992 assert_eq!(expected_relative_numbers, actual_relative_numbers);
31993 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
31994 for (display_row, relative_number) in expected_relative_numbers {
31995 assert_eq!(
31996 relative_number,
31997 snapshot
31998 .relative_line_delta(display_row, base_display_row, false)
31999 .unsigned_abs() as u32,
32000 );
32001 }
32002
32003 // test counting wrapped lines
32004 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
32005 .into_iter()
32006 .enumerate()
32007 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
32008 .filter(|(row, _)| *row != base_display_row)
32009 .collect_vec();
32010 let actual_relative_numbers = snapshot
32011 .calculate_relative_line_numbers(
32012 &(DisplayRow(0)..DisplayRow(24)),
32013 base_display_row,
32014 true,
32015 )
32016 .into_iter()
32017 .sorted()
32018 .collect_vec();
32019 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
32020 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
32021 for (display_row, relative_number) in expected_wrapped_relative_numbers {
32022 assert_eq!(
32023 relative_number,
32024 snapshot
32025 .relative_line_delta(display_row, base_display_row, true)
32026 .unsigned_abs() as u32,
32027 );
32028 }
32029 });
32030}
32031
32032#[gpui::test]
32033async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
32034 init_test(cx, |_| {});
32035 cx.update(|cx| {
32036 SettingsStore::update_global(cx, |store, cx| {
32037 store.update_user_settings(cx, |settings| {
32038 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
32039 enabled: Some(true),
32040 })
32041 });
32042 });
32043 });
32044 let mut cx = EditorTestContext::new(cx).await;
32045
32046 let line_height = cx.update_editor(|editor, window, cx| {
32047 editor
32048 .style(cx)
32049 .text
32050 .line_height_in_pixels(window.rem_size())
32051 });
32052
32053 let buffer = indoc! {"
32054 ˇfn foo() {
32055 let abc = 123;
32056 }
32057 struct Bar;
32058 impl Bar {
32059 fn new() -> Self {
32060 Self
32061 }
32062 }
32063 fn baz() {
32064 }
32065 "};
32066 cx.set_state(&buffer);
32067
32068 cx.update_editor(|e, _, cx| {
32069 e.buffer()
32070 .read(cx)
32071 .as_singleton()
32072 .unwrap()
32073 .update(cx, |buffer, cx| {
32074 buffer.set_language(Some(rust_lang()), cx);
32075 })
32076 });
32077
32078 let fn_foo = || empty_range(0, 0);
32079 let impl_bar = || empty_range(4, 0);
32080 let fn_new = || empty_range(5, 0);
32081
32082 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
32083 cx.update_editor(|e, window, cx| {
32084 e.scroll(
32085 gpui::Point {
32086 x: 0.,
32087 y: scroll_offset,
32088 },
32089 None,
32090 window,
32091 cx,
32092 );
32093 });
32094 cx.run_until_parked();
32095 cx.simulate_click(
32096 gpui::Point {
32097 x: px(0.),
32098 y: click_offset as f32 * line_height,
32099 },
32100 Modifiers::none(),
32101 );
32102 cx.run_until_parked();
32103 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
32104 };
32105 assert_eq!(
32106 scroll_and_click(
32107 4.5, // impl Bar is halfway off the screen
32108 0.0 // click top of screen
32109 ),
32110 // scrolled to impl Bar
32111 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32112 );
32113
32114 assert_eq!(
32115 scroll_and_click(
32116 4.5, // impl Bar is halfway off the screen
32117 0.25 // click middle of impl Bar
32118 ),
32119 // scrolled to impl Bar
32120 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32121 );
32122
32123 assert_eq!(
32124 scroll_and_click(
32125 4.5, // impl Bar is halfway off the screen
32126 1.5 // click below impl Bar (e.g. fn new())
32127 ),
32128 // scrolled to fn new() - this is below the impl Bar header which has persisted
32129 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
32130 );
32131
32132 assert_eq!(
32133 scroll_and_click(
32134 5.5, // fn new is halfway underneath impl Bar
32135 0.75 // click on the overlap of impl Bar and fn new()
32136 ),
32137 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32138 );
32139
32140 assert_eq!(
32141 scroll_and_click(
32142 5.5, // fn new is halfway underneath impl Bar
32143 1.25 // click on the visible part of fn new()
32144 ),
32145 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
32146 );
32147
32148 assert_eq!(
32149 scroll_and_click(
32150 1.5, // fn foo is halfway off the screen
32151 0.0 // click top of screen
32152 ),
32153 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
32154 );
32155
32156 assert_eq!(
32157 scroll_and_click(
32158 1.5, // fn foo is halfway off the screen
32159 0.75 // click visible part of let abc...
32160 )
32161 .0,
32162 // no change in scroll
32163 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
32164 (gpui::Point { x: 0., y: 1.5 })
32165 );
32166
32167 // Verify clicking at a specific x position within a sticky header places
32168 // the cursor at the corresponding column.
32169 let (text_origin_x, em_width) = cx.update_editor(|editor, _, _| {
32170 let position_map = editor.last_position_map.as_ref().unwrap();
32171 (
32172 position_map.text_hitbox.bounds.origin.x,
32173 position_map.em_layout_width,
32174 )
32175 });
32176
32177 // Click on "impl Bar {" sticky header at column 5 (the 'B' in 'Bar').
32178 // The text "impl Bar {" starts at column 0, so column 5 = 'B'.
32179 let click_x = text_origin_x + em_width * 5.5;
32180 cx.update_editor(|e, window, cx| {
32181 e.scroll(gpui::Point { x: 0., y: 4.5 }, None, window, cx);
32182 });
32183 cx.run_until_parked();
32184 cx.simulate_click(
32185 gpui::Point {
32186 x: click_x,
32187 y: 0.25 * line_height,
32188 },
32189 Modifiers::none(),
32190 );
32191 cx.run_until_parked();
32192 let (scroll_pos, selections) =
32193 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)));
32194 assert_eq!(scroll_pos, gpui::Point { x: 0., y: 4. });
32195 assert_eq!(selections, vec![empty_range(4, 5)]);
32196}
32197
32198#[gpui::test]
32199async fn test_clicking_sticky_header_sets_character_select_mode(cx: &mut TestAppContext) {
32200 init_test(cx, |_| {});
32201 cx.update(|cx| {
32202 SettingsStore::update_global(cx, |store, cx| {
32203 store.update_user_settings(cx, |settings| {
32204 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
32205 enabled: Some(true),
32206 })
32207 });
32208 });
32209 });
32210 let mut cx = EditorTestContext::new(cx).await;
32211
32212 let line_height = cx.update_editor(|editor, window, cx| {
32213 editor
32214 .style(cx)
32215 .text
32216 .line_height_in_pixels(window.rem_size())
32217 });
32218
32219 let buffer = indoc! {"
32220 fn foo() {
32221 let abc = 123;
32222 }
32223 ˇstruct Bar;
32224 "};
32225 cx.set_state(&buffer);
32226
32227 cx.update_editor(|editor, _, cx| {
32228 editor
32229 .buffer()
32230 .read(cx)
32231 .as_singleton()
32232 .unwrap()
32233 .update(cx, |buffer, cx| {
32234 buffer.set_language(Some(rust_lang()), cx);
32235 })
32236 });
32237
32238 let text_origin_x = cx.update_editor(|editor, _, _| {
32239 editor
32240 .last_position_map
32241 .as_ref()
32242 .unwrap()
32243 .text_hitbox
32244 .bounds
32245 .origin
32246 .x
32247 });
32248
32249 cx.update_editor(|editor, window, cx| {
32250 // Double click on `struct` to select it
32251 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 1), false, 2, window, cx);
32252 editor.end_selection(window, cx);
32253
32254 // Scroll down one row to make `fn foo() {` a sticky header
32255 editor.scroll(gpui::Point { x: 0., y: 1. }, None, window, cx);
32256 });
32257 cx.run_until_parked();
32258
32259 // Click at the start of the `fn foo() {` sticky header
32260 cx.simulate_click(
32261 gpui::Point {
32262 x: text_origin_x,
32263 y: 0.5 * line_height,
32264 },
32265 Modifiers::none(),
32266 );
32267 cx.run_until_parked();
32268
32269 // Shift-click at the end of `fn foo() {` to select the whole row
32270 cx.update_editor(|editor, window, cx| {
32271 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
32272 editor.end_selection(window, cx);
32273 });
32274 cx.run_until_parked();
32275
32276 let selections = cx.update_editor(|editor, _, cx| display_ranges(editor, cx));
32277 assert_eq!(
32278 selections,
32279 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 10)]
32280 );
32281}
32282
32283#[gpui::test]
32284async fn test_next_prev_reference(cx: &mut TestAppContext) {
32285 const CYCLE_POSITIONS: &[&'static str] = &[
32286 indoc! {"
32287 fn foo() {
32288 let ˇabc = 123;
32289 let x = abc + 1;
32290 let y = abc + 2;
32291 let z = abc + 2;
32292 }
32293 "},
32294 indoc! {"
32295 fn foo() {
32296 let abc = 123;
32297 let x = ˇabc + 1;
32298 let y = abc + 2;
32299 let z = abc + 2;
32300 }
32301 "},
32302 indoc! {"
32303 fn foo() {
32304 let abc = 123;
32305 let x = abc + 1;
32306 let y = ˇabc + 2;
32307 let z = abc + 2;
32308 }
32309 "},
32310 indoc! {"
32311 fn foo() {
32312 let abc = 123;
32313 let x = abc + 1;
32314 let y = abc + 2;
32315 let z = ˇabc + 2;
32316 }
32317 "},
32318 ];
32319
32320 init_test(cx, |_| {});
32321
32322 let mut cx = EditorLspTestContext::new_rust(
32323 lsp::ServerCapabilities {
32324 references_provider: Some(lsp::OneOf::Left(true)),
32325 ..Default::default()
32326 },
32327 cx,
32328 )
32329 .await;
32330
32331 // importantly, the cursor is in the middle
32332 cx.set_state(indoc! {"
32333 fn foo() {
32334 let aˇbc = 123;
32335 let x = abc + 1;
32336 let y = abc + 2;
32337 let z = abc + 2;
32338 }
32339 "});
32340
32341 let reference_ranges = [
32342 lsp::Position::new(1, 8),
32343 lsp::Position::new(2, 12),
32344 lsp::Position::new(3, 12),
32345 lsp::Position::new(4, 12),
32346 ]
32347 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
32348
32349 cx.lsp
32350 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
32351 Ok(Some(
32352 reference_ranges
32353 .map(|range| lsp::Location {
32354 uri: params.text_document_position.text_document.uri.clone(),
32355 range,
32356 })
32357 .to_vec(),
32358 ))
32359 });
32360
32361 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
32362 cx.update_editor(|editor, window, cx| {
32363 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
32364 })
32365 .unwrap()
32366 .await
32367 .unwrap()
32368 };
32369
32370 _move(Direction::Next, 1, &mut cx).await;
32371 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32372
32373 _move(Direction::Next, 1, &mut cx).await;
32374 cx.assert_editor_state(CYCLE_POSITIONS[2]);
32375
32376 _move(Direction::Next, 1, &mut cx).await;
32377 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32378
32379 // loops back to the start
32380 _move(Direction::Next, 1, &mut cx).await;
32381 cx.assert_editor_state(CYCLE_POSITIONS[0]);
32382
32383 // loops back to the end
32384 _move(Direction::Prev, 1, &mut cx).await;
32385 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32386
32387 _move(Direction::Prev, 1, &mut cx).await;
32388 cx.assert_editor_state(CYCLE_POSITIONS[2]);
32389
32390 _move(Direction::Prev, 1, &mut cx).await;
32391 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32392
32393 _move(Direction::Prev, 1, &mut cx).await;
32394 cx.assert_editor_state(CYCLE_POSITIONS[0]);
32395
32396 _move(Direction::Next, 3, &mut cx).await;
32397 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32398
32399 _move(Direction::Prev, 2, &mut cx).await;
32400 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32401}
32402
32403#[gpui::test]
32404async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
32405 init_test(cx, |_| {});
32406
32407 let (editor, cx) = cx.add_window_view(|window, cx| {
32408 let multi_buffer = MultiBuffer::build_multi(
32409 [
32410 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32411 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32412 ],
32413 cx,
32414 );
32415 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32416 });
32417
32418 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
32419 let buffer_ids = cx.multibuffer(|mb, cx| {
32420 mb.snapshot(cx)
32421 .excerpts()
32422 .map(|excerpt| excerpt.context.start.buffer_id)
32423 .collect::<Vec<_>>()
32424 });
32425
32426 cx.assert_excerpts_with_selections(indoc! {"
32427 [EXCERPT]
32428 ˇ1
32429 2
32430 3
32431 [EXCERPT]
32432 1
32433 2
32434 3
32435 "});
32436
32437 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
32438 cx.update_editor(|editor, window, cx| {
32439 editor.change_selections(None.into(), window, cx, |s| {
32440 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32441 });
32442 });
32443 cx.assert_excerpts_with_selections(indoc! {"
32444 [EXCERPT]
32445 1
32446 2ˇ
32447 3
32448 [EXCERPT]
32449 1
32450 2
32451 3
32452 "});
32453
32454 cx.update_editor(|editor, window, cx| {
32455 editor
32456 .select_all_matches(&SelectAllMatches, window, cx)
32457 .unwrap();
32458 });
32459 cx.assert_excerpts_with_selections(indoc! {"
32460 [EXCERPT]
32461 1
32462 2ˇ
32463 3
32464 [EXCERPT]
32465 1
32466 2ˇ
32467 3
32468 "});
32469
32470 cx.update_editor(|editor, window, cx| {
32471 editor.handle_input("X", window, cx);
32472 });
32473 cx.assert_excerpts_with_selections(indoc! {"
32474 [EXCERPT]
32475 1
32476 Xˇ
32477 3
32478 [EXCERPT]
32479 1
32480 Xˇ
32481 3
32482 "});
32483
32484 // Scenario 2: Select "2", then fold second buffer before insertion
32485 cx.update_multibuffer(|mb, cx| {
32486 for buffer_id in buffer_ids.iter() {
32487 let buffer = mb.buffer(*buffer_id).unwrap();
32488 buffer.update(cx, |buffer, cx| {
32489 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
32490 });
32491 }
32492 });
32493
32494 // Select "2" and select all matches
32495 cx.update_editor(|editor, window, cx| {
32496 editor.change_selections(None.into(), window, cx, |s| {
32497 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32498 });
32499 editor
32500 .select_all_matches(&SelectAllMatches, window, cx)
32501 .unwrap();
32502 });
32503
32504 // Fold second buffer - should remove selections from folded buffer
32505 cx.update_editor(|editor, _, cx| {
32506 editor.fold_buffer(buffer_ids[1], cx);
32507 });
32508 cx.assert_excerpts_with_selections(indoc! {"
32509 [EXCERPT]
32510 1
32511 2ˇ
32512 3
32513 [EXCERPT]
32514 [FOLDED]
32515 "});
32516
32517 // Insert text - should only affect first buffer
32518 cx.update_editor(|editor, window, cx| {
32519 editor.handle_input("Y", window, cx);
32520 });
32521 cx.update_editor(|editor, _, cx| {
32522 editor.unfold_buffer(buffer_ids[1], cx);
32523 });
32524 cx.assert_excerpts_with_selections(indoc! {"
32525 [EXCERPT]
32526 1
32527 Yˇ
32528 3
32529 [EXCERPT]
32530 1
32531 2
32532 3
32533 "});
32534
32535 // Scenario 3: Select "2", then fold first buffer before insertion
32536 cx.update_multibuffer(|mb, cx| {
32537 for buffer_id in buffer_ids.iter() {
32538 let buffer = mb.buffer(*buffer_id).unwrap();
32539 buffer.update(cx, |buffer, cx| {
32540 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
32541 });
32542 }
32543 });
32544
32545 // Select "2" and select all matches
32546 cx.update_editor(|editor, window, cx| {
32547 editor.change_selections(None.into(), window, cx, |s| {
32548 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32549 });
32550 editor
32551 .select_all_matches(&SelectAllMatches, window, cx)
32552 .unwrap();
32553 });
32554
32555 // Fold first buffer - should remove selections from folded buffer
32556 cx.update_editor(|editor, _, cx| {
32557 editor.fold_buffer(buffer_ids[0], cx);
32558 });
32559 cx.assert_excerpts_with_selections(indoc! {"
32560 [EXCERPT]
32561 [FOLDED]
32562 [EXCERPT]
32563 1
32564 2ˇ
32565 3
32566 "});
32567
32568 // Insert text - should only affect second buffer
32569 cx.update_editor(|editor, window, cx| {
32570 editor.handle_input("Z", window, cx);
32571 });
32572 cx.update_editor(|editor, _, cx| {
32573 editor.unfold_buffer(buffer_ids[0], cx);
32574 });
32575 cx.assert_excerpts_with_selections(indoc! {"
32576 [EXCERPT]
32577 1
32578 2
32579 3
32580 [EXCERPT]
32581 1
32582 Zˇ
32583 3
32584 "});
32585
32586 // Test correct folded header is selected upon fold
32587 cx.update_editor(|editor, _, cx| {
32588 editor.fold_buffer(buffer_ids[0], cx);
32589 editor.fold_buffer(buffer_ids[1], cx);
32590 });
32591 cx.assert_excerpts_with_selections(indoc! {"
32592 [EXCERPT]
32593 [FOLDED]
32594 [EXCERPT]
32595 ˇ[FOLDED]
32596 "});
32597
32598 // Test selection inside folded buffer unfolds it on type
32599 cx.update_editor(|editor, window, cx| {
32600 editor.handle_input("W", window, cx);
32601 });
32602 cx.update_editor(|editor, _, cx| {
32603 editor.unfold_buffer(buffer_ids[0], cx);
32604 });
32605 cx.assert_excerpts_with_selections(indoc! {"
32606 [EXCERPT]
32607 1
32608 2
32609 3
32610 [EXCERPT]
32611 Wˇ1
32612 Z
32613 3
32614 "});
32615}
32616
32617#[gpui::test]
32618async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
32619 init_test(cx, |_| {});
32620
32621 let (editor, cx) = cx.add_window_view(|window, cx| {
32622 let multi_buffer = MultiBuffer::build_multi(
32623 [
32624 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32625 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
32626 ],
32627 cx,
32628 );
32629 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32630 });
32631
32632 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
32633
32634 cx.assert_excerpts_with_selections(indoc! {"
32635 [EXCERPT]
32636 ˇ1
32637 2
32638 3
32639 [EXCERPT]
32640 1
32641 2
32642 3
32643 4
32644 5
32645 6
32646 7
32647 8
32648 9
32649 "});
32650
32651 cx.update_editor(|editor, window, cx| {
32652 editor.change_selections(None.into(), window, cx, |s| {
32653 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
32654 });
32655 });
32656
32657 cx.assert_excerpts_with_selections(indoc! {"
32658 [EXCERPT]
32659 1
32660 2
32661 3
32662 [EXCERPT]
32663 1
32664 2
32665 3
32666 4
32667 5
32668 6
32669 ˇ7
32670 8
32671 9
32672 "});
32673
32674 cx.update_editor(|editor, _window, cx| {
32675 editor.set_vertical_scroll_margin(0, cx);
32676 });
32677
32678 cx.update_editor(|editor, window, cx| {
32679 assert_eq!(editor.vertical_scroll_margin(), 0);
32680 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
32681 assert_eq!(
32682 editor.snapshot(window, cx).scroll_position(),
32683 gpui::Point::new(0., 12.0)
32684 );
32685 });
32686
32687 cx.update_editor(|editor, _window, cx| {
32688 editor.set_vertical_scroll_margin(3, cx);
32689 });
32690
32691 cx.update_editor(|editor, window, cx| {
32692 assert_eq!(editor.vertical_scroll_margin(), 3);
32693 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
32694 assert_eq!(
32695 editor.snapshot(window, cx).scroll_position(),
32696 gpui::Point::new(0., 9.0)
32697 );
32698 });
32699}
32700
32701#[gpui::test]
32702async fn test_find_references_single_case(cx: &mut TestAppContext) {
32703 init_test(cx, |_| {});
32704 let mut cx = EditorLspTestContext::new_rust(
32705 lsp::ServerCapabilities {
32706 references_provider: Some(lsp::OneOf::Left(true)),
32707 ..lsp::ServerCapabilities::default()
32708 },
32709 cx,
32710 )
32711 .await;
32712
32713 let before = indoc!(
32714 r#"
32715 fn main() {
32716 let aˇbc = 123;
32717 let xyz = abc;
32718 }
32719 "#
32720 );
32721 let after = indoc!(
32722 r#"
32723 fn main() {
32724 let abc = 123;
32725 let xyz = ˇabc;
32726 }
32727 "#
32728 );
32729
32730 cx.lsp
32731 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
32732 Ok(Some(vec![
32733 lsp::Location {
32734 uri: params.text_document_position.text_document.uri.clone(),
32735 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
32736 },
32737 lsp::Location {
32738 uri: params.text_document_position.text_document.uri,
32739 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
32740 },
32741 ]))
32742 });
32743
32744 cx.set_state(before);
32745
32746 let action = FindAllReferences {
32747 always_open_multibuffer: false,
32748 };
32749
32750 let navigated = cx
32751 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
32752 .expect("should have spawned a task")
32753 .await
32754 .unwrap();
32755
32756 assert_eq!(navigated, Navigated::No);
32757
32758 cx.run_until_parked();
32759
32760 cx.assert_editor_state(after);
32761}
32762
32763#[gpui::test]
32764async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
32765 init_test(cx, |settings| {
32766 settings.defaults.tab_size = Some(2.try_into().unwrap());
32767 });
32768
32769 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
32770 let mut cx = EditorTestContext::new(cx).await;
32771 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
32772
32773 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
32774 cx.set_state(indoc! {"
32775 - [ ] taskˇ
32776 "});
32777 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32778 cx.wait_for_autoindent_applied().await;
32779 cx.assert_editor_state(indoc! {"
32780 - [ ] task
32781 - [ ] ˇ
32782 "});
32783
32784 // Case 2: Works with checked task items too
32785 cx.set_state(indoc! {"
32786 - [x] completed taskˇ
32787 "});
32788 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32789 cx.wait_for_autoindent_applied().await;
32790 cx.assert_editor_state(indoc! {"
32791 - [x] completed task
32792 - [ ] ˇ
32793 "});
32794
32795 // Case 2.1: Works with uppercase checked marker too
32796 cx.set_state(indoc! {"
32797 - [X] completed taskˇ
32798 "});
32799 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32800 cx.wait_for_autoindent_applied().await;
32801 cx.assert_editor_state(indoc! {"
32802 - [X] completed task
32803 - [ ] ˇ
32804 "});
32805
32806 // Case 3: Cursor position doesn't matter - content after marker is what counts
32807 cx.set_state(indoc! {"
32808 - [ ] taˇsk
32809 "});
32810 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32811 cx.wait_for_autoindent_applied().await;
32812 cx.assert_editor_state(indoc! {"
32813 - [ ] ta
32814 - [ ] ˇsk
32815 "});
32816
32817 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
32818 cx.set_state(indoc! {"
32819 - [ ] ˇ
32820 "});
32821 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32822 cx.wait_for_autoindent_applied().await;
32823 cx.assert_editor_state(
32824 indoc! {"
32825 - [ ]$$
32826 ˇ
32827 "}
32828 .replace("$", " ")
32829 .as_str(),
32830 );
32831
32832 // Case 5: Adding newline with content adds marker preserving indentation
32833 cx.set_state(indoc! {"
32834 - [ ] task
32835 - [ ] indentedˇ
32836 "});
32837 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32838 cx.wait_for_autoindent_applied().await;
32839 cx.assert_editor_state(indoc! {"
32840 - [ ] task
32841 - [ ] indented
32842 - [ ] ˇ
32843 "});
32844
32845 // Case 6: Adding newline with cursor right after prefix, unindents
32846 cx.set_state(indoc! {"
32847 - [ ] task
32848 - [ ] sub task
32849 - [ ] ˇ
32850 "});
32851 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32852 cx.wait_for_autoindent_applied().await;
32853 cx.assert_editor_state(indoc! {"
32854 - [ ] task
32855 - [ ] sub task
32856 - [ ] ˇ
32857 "});
32858 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32859 cx.wait_for_autoindent_applied().await;
32860
32861 // Case 7: Adding newline with cursor right after prefix, removes marker
32862 cx.assert_editor_state(indoc! {"
32863 - [ ] task
32864 - [ ] sub task
32865 - [ ] ˇ
32866 "});
32867 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32868 cx.wait_for_autoindent_applied().await;
32869 cx.assert_editor_state(indoc! {"
32870 - [ ] task
32871 - [ ] sub task
32872 ˇ
32873 "});
32874
32875 // Case 8: Cursor before or inside prefix does not add marker
32876 cx.set_state(indoc! {"
32877 ˇ- [ ] task
32878 "});
32879 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32880 cx.wait_for_autoindent_applied().await;
32881 cx.assert_editor_state(indoc! {"
32882
32883 ˇ- [ ] task
32884 "});
32885
32886 cx.set_state(indoc! {"
32887 - [ˇ ] task
32888 "});
32889 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32890 cx.wait_for_autoindent_applied().await;
32891 cx.assert_editor_state(indoc! {"
32892 - [
32893 ˇ
32894 ] task
32895 "});
32896}
32897
32898#[gpui::test]
32899async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
32900 init_test(cx, |settings| {
32901 settings.defaults.tab_size = Some(2.try_into().unwrap());
32902 });
32903
32904 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
32905 let mut cx = EditorTestContext::new(cx).await;
32906 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
32907
32908 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
32909 cx.set_state(indoc! {"
32910 - itemˇ
32911 "});
32912 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32913 cx.wait_for_autoindent_applied().await;
32914 cx.assert_editor_state(indoc! {"
32915 - item
32916 - ˇ
32917 "});
32918
32919 // Case 2: Works with different markers
32920 cx.set_state(indoc! {"
32921 * starred itemˇ
32922 "});
32923 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32924 cx.wait_for_autoindent_applied().await;
32925 cx.assert_editor_state(indoc! {"
32926 * starred item
32927 * ˇ
32928 "});
32929
32930 cx.set_state(indoc! {"
32931 + plus itemˇ
32932 "});
32933 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32934 cx.wait_for_autoindent_applied().await;
32935 cx.assert_editor_state(indoc! {"
32936 + plus item
32937 + ˇ
32938 "});
32939
32940 // Case 3: Cursor position doesn't matter - content after marker is what counts
32941 cx.set_state(indoc! {"
32942 - itˇem
32943 "});
32944 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32945 cx.wait_for_autoindent_applied().await;
32946 cx.assert_editor_state(indoc! {"
32947 - it
32948 - ˇem
32949 "});
32950
32951 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
32952 cx.set_state(indoc! {"
32953 - ˇ
32954 "});
32955 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32956 cx.wait_for_autoindent_applied().await;
32957 cx.assert_editor_state(
32958 indoc! {"
32959 - $
32960 ˇ
32961 "}
32962 .replace("$", " ")
32963 .as_str(),
32964 );
32965
32966 // Case 5: Adding newline with content adds marker preserving indentation
32967 cx.set_state(indoc! {"
32968 - item
32969 - indentedˇ
32970 "});
32971 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32972 cx.wait_for_autoindent_applied().await;
32973 cx.assert_editor_state(indoc! {"
32974 - item
32975 - indented
32976 - ˇ
32977 "});
32978
32979 // Case 6: Adding newline with cursor right after marker, unindents
32980 cx.set_state(indoc! {"
32981 - item
32982 - sub item
32983 - ˇ
32984 "});
32985 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32986 cx.wait_for_autoindent_applied().await;
32987 cx.assert_editor_state(indoc! {"
32988 - item
32989 - sub item
32990 - ˇ
32991 "});
32992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32993 cx.wait_for_autoindent_applied().await;
32994
32995 // Case 7: Adding newline with cursor right after marker, removes marker
32996 cx.assert_editor_state(indoc! {"
32997 - item
32998 - sub item
32999 - ˇ
33000 "});
33001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33002 cx.wait_for_autoindent_applied().await;
33003 cx.assert_editor_state(indoc! {"
33004 - item
33005 - sub item
33006 ˇ
33007 "});
33008
33009 // Case 8: Cursor before or inside prefix does not add marker
33010 cx.set_state(indoc! {"
33011 ˇ- item
33012 "});
33013 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33014 cx.wait_for_autoindent_applied().await;
33015 cx.assert_editor_state(indoc! {"
33016
33017 ˇ- item
33018 "});
33019
33020 cx.set_state(indoc! {"
33021 -ˇ item
33022 "});
33023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33024 cx.wait_for_autoindent_applied().await;
33025 cx.assert_editor_state(indoc! {"
33026 -
33027 ˇitem
33028 "});
33029}
33030
33031#[gpui::test]
33032async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
33033 init_test(cx, |settings| {
33034 settings.defaults.tab_size = Some(2.try_into().unwrap());
33035 });
33036
33037 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33038 let mut cx = EditorTestContext::new(cx).await;
33039 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33040
33041 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
33042 cx.set_state(indoc! {"
33043 1. first itemˇ
33044 "});
33045 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33046 cx.wait_for_autoindent_applied().await;
33047 cx.assert_editor_state(indoc! {"
33048 1. first item
33049 2. ˇ
33050 "});
33051
33052 // Case 2: Works with larger numbers
33053 cx.set_state(indoc! {"
33054 10. tenth itemˇ
33055 "});
33056 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33057 cx.wait_for_autoindent_applied().await;
33058 cx.assert_editor_state(indoc! {"
33059 10. tenth item
33060 11. ˇ
33061 "});
33062
33063 // Case 3: Cursor position doesn't matter - content after marker is what counts
33064 cx.set_state(indoc! {"
33065 1. itˇem
33066 "});
33067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33068 cx.wait_for_autoindent_applied().await;
33069 cx.assert_editor_state(indoc! {"
33070 1. it
33071 2. ˇem
33072 "});
33073
33074 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
33075 cx.set_state(indoc! {"
33076 1. ˇ
33077 "});
33078 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33079 cx.wait_for_autoindent_applied().await;
33080 cx.assert_editor_state(
33081 indoc! {"
33082 1. $
33083 ˇ
33084 "}
33085 .replace("$", " ")
33086 .as_str(),
33087 );
33088
33089 // Case 5: Adding newline with content adds marker preserving indentation
33090 cx.set_state(indoc! {"
33091 1. item
33092 2. indentedˇ
33093 "});
33094 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33095 cx.wait_for_autoindent_applied().await;
33096 cx.assert_editor_state(indoc! {"
33097 1. item
33098 2. indented
33099 3. ˇ
33100 "});
33101
33102 // Case 6: Adding newline with cursor right after marker, unindents
33103 cx.set_state(indoc! {"
33104 1. item
33105 2. sub item
33106 3. ˇ
33107 "});
33108 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33109 cx.wait_for_autoindent_applied().await;
33110 cx.assert_editor_state(indoc! {"
33111 1. item
33112 2. sub item
33113 1. ˇ
33114 "});
33115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33116 cx.wait_for_autoindent_applied().await;
33117
33118 // Case 7: Adding newline with cursor right after marker, removes marker
33119 cx.assert_editor_state(indoc! {"
33120 1. item
33121 2. sub item
33122 1. ˇ
33123 "});
33124 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33125 cx.wait_for_autoindent_applied().await;
33126 cx.assert_editor_state(indoc! {"
33127 1. item
33128 2. sub item
33129 ˇ
33130 "});
33131
33132 // Case 8: Cursor before or inside prefix does not add marker
33133 cx.set_state(indoc! {"
33134 ˇ1. item
33135 "});
33136 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33137 cx.wait_for_autoindent_applied().await;
33138 cx.assert_editor_state(indoc! {"
33139
33140 ˇ1. item
33141 "});
33142
33143 cx.set_state(indoc! {"
33144 1ˇ. item
33145 "});
33146 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33147 cx.wait_for_autoindent_applied().await;
33148 cx.assert_editor_state(indoc! {"
33149 1
33150 ˇ. item
33151 "});
33152}
33153
33154#[gpui::test]
33155async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
33156 init_test(cx, |settings| {
33157 settings.defaults.tab_size = Some(2.try_into().unwrap());
33158 });
33159
33160 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33161 let mut cx = EditorTestContext::new(cx).await;
33162 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33163
33164 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
33165 cx.set_state(indoc! {"
33166 1. first item
33167 1. sub first item
33168 2. sub second item
33169 3. ˇ
33170 "});
33171 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33172 cx.wait_for_autoindent_applied().await;
33173 cx.assert_editor_state(indoc! {"
33174 1. first item
33175 1. sub first item
33176 2. sub second item
33177 1. ˇ
33178 "});
33179}
33180
33181#[gpui::test]
33182async fn test_tab_list_indent(cx: &mut TestAppContext) {
33183 init_test(cx, |settings| {
33184 settings.defaults.tab_size = Some(2.try_into().unwrap());
33185 });
33186
33187 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33188 let mut cx = EditorTestContext::new(cx).await;
33189 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33190
33191 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
33192 cx.set_state(indoc! {"
33193 - ˇitem
33194 "});
33195 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33196 cx.wait_for_autoindent_applied().await;
33197 let expected = indoc! {"
33198 $$- ˇitem
33199 "};
33200 cx.assert_editor_state(expected.replace("$", " ").as_str());
33201
33202 // Case 2: Task list - cursor after prefix
33203 cx.set_state(indoc! {"
33204 - [ ] ˇtask
33205 "});
33206 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33207 cx.wait_for_autoindent_applied().await;
33208 let expected = indoc! {"
33209 $$- [ ] ˇtask
33210 "};
33211 cx.assert_editor_state(expected.replace("$", " ").as_str());
33212
33213 // Case 3: Ordered list - cursor after prefix
33214 cx.set_state(indoc! {"
33215 1. ˇfirst
33216 "});
33217 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33218 cx.wait_for_autoindent_applied().await;
33219 let expected = indoc! {"
33220 $$1. ˇfirst
33221 "};
33222 cx.assert_editor_state(expected.replace("$", " ").as_str());
33223
33224 // Case 4: With existing indentation - adds more indent
33225 let initial = indoc! {"
33226 $$- ˇitem
33227 "};
33228 cx.set_state(initial.replace("$", " ").as_str());
33229 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33230 cx.wait_for_autoindent_applied().await;
33231 let expected = indoc! {"
33232 $$$$- ˇitem
33233 "};
33234 cx.assert_editor_state(expected.replace("$", " ").as_str());
33235
33236 // Case 5: Empty list item
33237 cx.set_state(indoc! {"
33238 - ˇ
33239 "});
33240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33241 cx.wait_for_autoindent_applied().await;
33242 let expected = indoc! {"
33243 $$- ˇ
33244 "};
33245 cx.assert_editor_state(expected.replace("$", " ").as_str());
33246
33247 // Case 6: Cursor at end of line with content
33248 cx.set_state(indoc! {"
33249 - itemˇ
33250 "});
33251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33252 cx.wait_for_autoindent_applied().await;
33253 let expected = indoc! {"
33254 $$- itemˇ
33255 "};
33256 cx.assert_editor_state(expected.replace("$", " ").as_str());
33257
33258 // Case 7: Cursor at start of list item, indents it
33259 cx.set_state(indoc! {"
33260 - item
33261 ˇ - sub item
33262 "});
33263 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33264 cx.wait_for_autoindent_applied().await;
33265 let expected = indoc! {"
33266 - item
33267 ˇ - sub item
33268 "};
33269 cx.assert_editor_state(expected);
33270
33271 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
33272 cx.update_editor(|_, _, cx| {
33273 SettingsStore::update_global(cx, |store, cx| {
33274 store.update_user_settings(cx, |settings| {
33275 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
33276 });
33277 });
33278 });
33279 cx.set_state(indoc! {"
33280 - item
33281 ˇ - sub item
33282 "});
33283 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33284 cx.wait_for_autoindent_applied().await;
33285 let expected = indoc! {"
33286 - item
33287 ˇ- sub item
33288 "};
33289 cx.assert_editor_state(expected);
33290}
33291
33292#[gpui::test]
33293async fn test_local_worktree_trust(cx: &mut TestAppContext) {
33294 init_test(cx, |_| {});
33295 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
33296
33297 cx.update(|cx| {
33298 SettingsStore::update_global(cx, |store, cx| {
33299 store.update_user_settings(cx, |settings| {
33300 settings.project.all_languages.defaults.inlay_hints =
33301 Some(InlayHintSettingsContent {
33302 enabled: Some(true),
33303 ..InlayHintSettingsContent::default()
33304 });
33305 });
33306 });
33307 });
33308
33309 let fs = FakeFs::new(cx.executor());
33310 fs.insert_tree(
33311 path!("/project"),
33312 json!({
33313 ".zed": {
33314 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
33315 },
33316 "main.rs": "fn main() {}"
33317 }),
33318 )
33319 .await;
33320
33321 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
33322 let server_name = "override-rust-analyzer";
33323 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
33324
33325 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
33326 language_registry.add(rust_lang());
33327
33328 let capabilities = lsp::ServerCapabilities {
33329 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
33330 ..lsp::ServerCapabilities::default()
33331 };
33332 let mut fake_language_servers = language_registry.register_fake_lsp(
33333 "Rust",
33334 FakeLspAdapter {
33335 name: server_name,
33336 capabilities,
33337 initializer: Some(Box::new({
33338 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
33339 move |fake_server| {
33340 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
33341 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
33342 move |_params, _| {
33343 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
33344 async move {
33345 Ok(Some(vec![lsp::InlayHint {
33346 position: lsp::Position::new(0, 0),
33347 label: lsp::InlayHintLabel::String("hint".to_string()),
33348 kind: None,
33349 text_edits: None,
33350 tooltip: None,
33351 padding_left: None,
33352 padding_right: None,
33353 data: None,
33354 }]))
33355 }
33356 },
33357 );
33358 }
33359 })),
33360 ..FakeLspAdapter::default()
33361 },
33362 );
33363
33364 cx.run_until_parked();
33365
33366 let worktree_id = project.read_with(cx, |project, cx| {
33367 project
33368 .worktrees(cx)
33369 .next()
33370 .map(|wt| wt.read(cx).id())
33371 .expect("should have a worktree")
33372 });
33373 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
33374
33375 let trusted_worktrees =
33376 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
33377
33378 let can_trust = trusted_worktrees.update(cx, |store, cx| {
33379 store.can_trust(&worktree_store, worktree_id, cx)
33380 });
33381 assert!(!can_trust, "worktree should be restricted initially");
33382
33383 let buffer_before_approval = project
33384 .update(cx, |project, cx| {
33385 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
33386 })
33387 .await
33388 .unwrap();
33389
33390 let (editor, cx) = cx.add_window_view(|window, cx| {
33391 Editor::new(
33392 EditorMode::full(),
33393 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
33394 Some(project.clone()),
33395 window,
33396 cx,
33397 )
33398 });
33399 cx.run_until_parked();
33400 let fake_language_server = fake_language_servers.next();
33401
33402 cx.read(|cx| {
33403 assert_eq!(
33404 language::language_settings::LanguageSettings::for_buffer(
33405 buffer_before_approval.read(cx),
33406 cx
33407 )
33408 .language_servers,
33409 ["...".to_string()],
33410 "local .zed/settings.json must not apply before trust approval"
33411 )
33412 });
33413
33414 editor.update_in(cx, |editor, window, cx| {
33415 editor.handle_input("1", window, cx);
33416 });
33417 cx.run_until_parked();
33418 cx.executor()
33419 .advance_clock(std::time::Duration::from_secs(1));
33420 assert_eq!(
33421 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
33422 0,
33423 "inlay hints must not be queried before trust approval"
33424 );
33425
33426 trusted_worktrees.update(cx, |store, cx| {
33427 store.trust(
33428 &worktree_store,
33429 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
33430 cx,
33431 );
33432 });
33433 cx.run_until_parked();
33434
33435 cx.read(|cx| {
33436 assert_eq!(
33437 language::language_settings::LanguageSettings::for_buffer(
33438 buffer_before_approval.read(cx),
33439 cx
33440 )
33441 .language_servers,
33442 ["override-rust-analyzer".to_string()],
33443 "local .zed/settings.json should apply after trust approval"
33444 )
33445 });
33446 let _fake_language_server = fake_language_server.await.unwrap();
33447 editor.update_in(cx, |editor, window, cx| {
33448 editor.handle_input("1", window, cx);
33449 });
33450 cx.run_until_parked();
33451 cx.executor()
33452 .advance_clock(std::time::Duration::from_secs(1));
33453 assert!(
33454 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
33455 "inlay hints should be queried after trust approval"
33456 );
33457
33458 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
33459 store.can_trust(&worktree_store, worktree_id, cx)
33460 });
33461 assert!(can_trust_after, "worktree should be trusted after trust()");
33462}
33463
33464#[gpui::test]
33465fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
33466 // This test reproduces a bug where drawing an editor at a position above the viewport
33467 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
33468 // causes an infinite loop in blocks_in_range.
33469 //
33470 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
33471 // the content mask intersection produces visible_bounds with origin at the viewport top.
33472 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
33473 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
33474 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
33475 init_test(cx, |_| {});
33476
33477 let window = cx.add_window(|_, _| gpui::Empty);
33478 let mut cx = VisualTestContext::from_window(*window, cx);
33479
33480 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
33481 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
33482
33483 // Simulate a small viewport (500x500 pixels at origin 0,0)
33484 cx.simulate_resize(gpui::size(px(500.), px(500.)));
33485
33486 // Draw the editor at a very negative Y position, simulating an editor that's been
33487 // scrolled way above the visible viewport (like in a List that has scrolled past it).
33488 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
33489 // This should NOT hang - it should just render nothing.
33490 cx.draw(
33491 gpui::point(px(0.), px(-10000.)),
33492 gpui::size(px(500.), px(3000.)),
33493 |_, _| editor.clone().into_any_element(),
33494 );
33495
33496 // If we get here without hanging, the test passes
33497}
33498
33499#[gpui::test]
33500async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
33501 init_test(cx, |_| {});
33502
33503 let fs = FakeFs::new(cx.executor());
33504 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33505 .await;
33506
33507 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33508 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33509 let workspace = window
33510 .read_with(cx, |mw, _| mw.workspace().clone())
33511 .unwrap();
33512 let cx = &mut VisualTestContext::from_window(*window, cx);
33513
33514 let editor = workspace
33515 .update_in(cx, |workspace, window, cx| {
33516 workspace.open_abs_path(
33517 PathBuf::from(path!("/root/file.txt")),
33518 OpenOptions::default(),
33519 window,
33520 cx,
33521 )
33522 })
33523 .await
33524 .unwrap()
33525 .downcast::<Editor>()
33526 .unwrap();
33527
33528 // Enable diff review button mode
33529 editor.update(cx, |editor, cx| {
33530 editor.set_show_diff_review_button(true, cx);
33531 });
33532
33533 // Initially, no indicator should be present
33534 editor.update(cx, |editor, _cx| {
33535 assert!(
33536 editor.gutter_diff_review_indicator.0.is_none(),
33537 "Indicator should be None initially"
33538 );
33539 });
33540}
33541
33542#[gpui::test]
33543async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
33544 init_test(cx, |_| {});
33545
33546 // Register DisableAiSettings and set disable_ai to true
33547 cx.update(|cx| {
33548 project::DisableAiSettings::register(cx);
33549 project::DisableAiSettings::override_global(
33550 project::DisableAiSettings { disable_ai: true },
33551 cx,
33552 );
33553 });
33554
33555 let fs = FakeFs::new(cx.executor());
33556 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33557 .await;
33558
33559 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33560 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33561 let workspace = window
33562 .read_with(cx, |mw, _| mw.workspace().clone())
33563 .unwrap();
33564 let cx = &mut VisualTestContext::from_window(*window, cx);
33565
33566 let editor = workspace
33567 .update_in(cx, |workspace, window, cx| {
33568 workspace.open_abs_path(
33569 PathBuf::from(path!("/root/file.txt")),
33570 OpenOptions::default(),
33571 window,
33572 cx,
33573 )
33574 })
33575 .await
33576 .unwrap()
33577 .downcast::<Editor>()
33578 .unwrap();
33579
33580 // Enable diff review button mode
33581 editor.update(cx, |editor, cx| {
33582 editor.set_show_diff_review_button(true, cx);
33583 });
33584
33585 // Verify AI is disabled
33586 cx.read(|cx| {
33587 assert!(
33588 project::DisableAiSettings::get_global(cx).disable_ai,
33589 "AI should be disabled"
33590 );
33591 });
33592
33593 // The indicator should not be created when AI is disabled
33594 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
33595 editor.update(cx, |editor, _cx| {
33596 assert!(
33597 editor.gutter_diff_review_indicator.0.is_none(),
33598 "Indicator should be None when AI is disabled"
33599 );
33600 });
33601}
33602
33603#[gpui::test]
33604async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
33605 init_test(cx, |_| {});
33606
33607 // Register DisableAiSettings and set disable_ai to false
33608 cx.update(|cx| {
33609 project::DisableAiSettings::register(cx);
33610 project::DisableAiSettings::override_global(
33611 project::DisableAiSettings { disable_ai: false },
33612 cx,
33613 );
33614 });
33615
33616 let fs = FakeFs::new(cx.executor());
33617 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33618 .await;
33619
33620 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33621 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33622 let workspace = window
33623 .read_with(cx, |mw, _| mw.workspace().clone())
33624 .unwrap();
33625 let cx = &mut VisualTestContext::from_window(*window, cx);
33626
33627 let editor = workspace
33628 .update_in(cx, |workspace, window, cx| {
33629 workspace.open_abs_path(
33630 PathBuf::from(path!("/root/file.txt")),
33631 OpenOptions::default(),
33632 window,
33633 cx,
33634 )
33635 })
33636 .await
33637 .unwrap()
33638 .downcast::<Editor>()
33639 .unwrap();
33640
33641 // Enable diff review button mode
33642 editor.update(cx, |editor, cx| {
33643 editor.set_show_diff_review_button(true, cx);
33644 });
33645
33646 // Verify AI is enabled
33647 cx.read(|cx| {
33648 assert!(
33649 !project::DisableAiSettings::get_global(cx).disable_ai,
33650 "AI should be enabled"
33651 );
33652 });
33653
33654 // The show_diff_review_button flag should be true
33655 editor.update(cx, |editor, _cx| {
33656 assert!(
33657 editor.show_diff_review_button(),
33658 "show_diff_review_button should be true"
33659 );
33660 });
33661}
33662
33663/// Helper function to create a DiffHunkKey for testing.
33664/// Uses Anchor::Min as a placeholder anchor since these tests don't need
33665/// real buffer positioning.
33666fn test_hunk_key(file_path: &str) -> DiffHunkKey {
33667 DiffHunkKey {
33668 file_path: if file_path.is_empty() {
33669 Arc::from(util::rel_path::RelPath::empty())
33670 } else {
33671 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
33672 },
33673 hunk_start_anchor: Anchor::Min,
33674 }
33675}
33676
33677/// Helper function to create a DiffHunkKey with a specific anchor for testing.
33678fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
33679 DiffHunkKey {
33680 file_path: if file_path.is_empty() {
33681 Arc::from(util::rel_path::RelPath::empty())
33682 } else {
33683 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
33684 },
33685 hunk_start_anchor: anchor,
33686 }
33687}
33688
33689/// Helper function to add a review comment with default anchors for testing.
33690fn add_test_comment(
33691 editor: &mut Editor,
33692 key: DiffHunkKey,
33693 comment: &str,
33694 cx: &mut Context<Editor>,
33695) -> usize {
33696 editor.add_review_comment(key, comment.to_string(), Anchor::Min..Anchor::Max, cx)
33697}
33698
33699#[gpui::test]
33700fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
33701 init_test(cx, |_| {});
33702
33703 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33704
33705 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33706 let key = test_hunk_key("");
33707
33708 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
33709
33710 let snapshot = editor.buffer().read(cx).snapshot(cx);
33711 assert_eq!(editor.total_review_comment_count(), 1);
33712 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
33713
33714 let comments = editor.comments_for_hunk(&key, &snapshot);
33715 assert_eq!(comments.len(), 1);
33716 assert_eq!(comments[0].comment, "Test comment");
33717 assert_eq!(comments[0].id, id);
33718 });
33719}
33720
33721#[gpui::test]
33722fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
33723 init_test(cx, |_| {});
33724
33725 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33726
33727 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33728 let snapshot = editor.buffer().read(cx).snapshot(cx);
33729 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
33730 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
33731 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
33732 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
33733
33734 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
33735 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
33736
33737 let snapshot = editor.buffer().read(cx).snapshot(cx);
33738 assert_eq!(editor.total_review_comment_count(), 2);
33739 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
33740 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
33741
33742 assert_eq!(
33743 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
33744 "Comment for file1"
33745 );
33746 assert_eq!(
33747 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
33748 "Comment for file2"
33749 );
33750 });
33751}
33752
33753#[gpui::test]
33754fn test_review_comment_remove(cx: &mut TestAppContext) {
33755 init_test(cx, |_| {});
33756
33757 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33758
33759 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33760 let key = test_hunk_key("");
33761
33762 let id = add_test_comment(editor, key, "To be removed", cx);
33763
33764 assert_eq!(editor.total_review_comment_count(), 1);
33765
33766 let removed = editor.remove_review_comment(id, cx);
33767 assert!(removed);
33768 assert_eq!(editor.total_review_comment_count(), 0);
33769
33770 // Try to remove again
33771 let removed_again = editor.remove_review_comment(id, cx);
33772 assert!(!removed_again);
33773 });
33774}
33775
33776#[gpui::test]
33777fn test_review_comment_update(cx: &mut TestAppContext) {
33778 init_test(cx, |_| {});
33779
33780 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33781
33782 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33783 let key = test_hunk_key("");
33784
33785 let id = add_test_comment(editor, key.clone(), "Original text", cx);
33786
33787 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
33788 assert!(updated);
33789
33790 let snapshot = editor.buffer().read(cx).snapshot(cx);
33791 let comments = editor.comments_for_hunk(&key, &snapshot);
33792 assert_eq!(comments[0].comment, "Updated text");
33793 assert!(!comments[0].is_editing); // Should clear editing flag
33794 });
33795}
33796
33797#[gpui::test]
33798fn test_review_comment_take_all(cx: &mut TestAppContext) {
33799 init_test(cx, |_| {});
33800
33801 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33802
33803 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33804 let snapshot = editor.buffer().read(cx).snapshot(cx);
33805 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
33806 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
33807 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
33808 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
33809
33810 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
33811 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
33812 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
33813
33814 // IDs should be sequential starting from 0
33815 assert_eq!(id1, 0);
33816 assert_eq!(id2, 1);
33817 assert_eq!(id3, 2);
33818
33819 assert_eq!(editor.total_review_comment_count(), 3);
33820
33821 let taken = editor.take_all_review_comments(cx);
33822
33823 // Should have 2 entries (one per hunk)
33824 assert_eq!(taken.len(), 2);
33825
33826 // Total comments should be 3
33827 let total: usize = taken
33828 .iter()
33829 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
33830 .sum();
33831 assert_eq!(total, 3);
33832
33833 // Storage should be empty
33834 assert_eq!(editor.total_review_comment_count(), 0);
33835
33836 // After taking all comments, ID counter should reset
33837 // New comments should get IDs starting from 0 again
33838 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
33839 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
33840
33841 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
33842 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
33843 });
33844}
33845
33846#[gpui::test]
33847fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
33848 init_test(cx, |_| {});
33849
33850 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33851
33852 // Show overlay
33853 editor
33854 .update(cx, |editor, window, cx| {
33855 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33856 })
33857 .unwrap();
33858
33859 // Verify overlay is shown
33860 editor
33861 .update(cx, |editor, _window, cx| {
33862 assert!(!editor.diff_review_overlays.is_empty());
33863 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
33864 assert!(editor.diff_review_prompt_editor().is_some());
33865 })
33866 .unwrap();
33867
33868 // Dismiss overlay
33869 editor
33870 .update(cx, |editor, _window, cx| {
33871 editor.dismiss_all_diff_review_overlays(cx);
33872 })
33873 .unwrap();
33874
33875 // Verify overlay is dismissed
33876 editor
33877 .update(cx, |editor, _window, cx| {
33878 assert!(editor.diff_review_overlays.is_empty());
33879 assert_eq!(editor.diff_review_line_range(cx), None);
33880 assert!(editor.diff_review_prompt_editor().is_none());
33881 })
33882 .unwrap();
33883}
33884
33885#[gpui::test]
33886fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
33887 init_test(cx, |_| {});
33888
33889 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33890
33891 // Show overlay
33892 editor
33893 .update(cx, |editor, window, cx| {
33894 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33895 })
33896 .unwrap();
33897
33898 // Verify overlay is shown
33899 editor
33900 .update(cx, |editor, _window, _cx| {
33901 assert!(!editor.diff_review_overlays.is_empty());
33902 })
33903 .unwrap();
33904
33905 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
33906 editor
33907 .update(cx, |editor, window, cx| {
33908 editor.dismiss_menus_and_popups(true, window, cx);
33909 })
33910 .unwrap();
33911
33912 // Verify overlay is dismissed
33913 editor
33914 .update(cx, |editor, _window, _cx| {
33915 assert!(editor.diff_review_overlays.is_empty());
33916 })
33917 .unwrap();
33918}
33919
33920#[gpui::test]
33921fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
33922 init_test(cx, |_| {});
33923
33924 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33925
33926 // Show overlay
33927 editor
33928 .update(cx, |editor, window, cx| {
33929 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33930 })
33931 .unwrap();
33932
33933 // Try to submit without typing anything (empty comment)
33934 editor
33935 .update(cx, |editor, window, cx| {
33936 editor.submit_diff_review_comment(window, cx);
33937 })
33938 .unwrap();
33939
33940 // Verify no comment was added
33941 editor
33942 .update(cx, |editor, _window, _cx| {
33943 assert_eq!(editor.total_review_comment_count(), 0);
33944 })
33945 .unwrap();
33946
33947 // Try to submit with whitespace-only comment
33948 editor
33949 .update(cx, |editor, window, cx| {
33950 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
33951 prompt_editor.update(cx, |pe, cx| {
33952 pe.insert(" \n\t ", window, cx);
33953 });
33954 }
33955 editor.submit_diff_review_comment(window, cx);
33956 })
33957 .unwrap();
33958
33959 // Verify still no comment was added
33960 editor
33961 .update(cx, |editor, _window, _cx| {
33962 assert_eq!(editor.total_review_comment_count(), 0);
33963 })
33964 .unwrap();
33965}
33966
33967#[gpui::test]
33968fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
33969 init_test(cx, |_| {});
33970
33971 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33972
33973 // Add a comment directly
33974 let comment_id = editor
33975 .update(cx, |editor, _window, cx| {
33976 let key = test_hunk_key("");
33977 add_test_comment(editor, key, "Original comment", cx)
33978 })
33979 .unwrap();
33980
33981 // Set comment to editing mode
33982 editor
33983 .update(cx, |editor, _window, cx| {
33984 editor.set_comment_editing(comment_id, true, cx);
33985 })
33986 .unwrap();
33987
33988 // Verify editing flag is set
33989 editor
33990 .update(cx, |editor, _window, cx| {
33991 let key = test_hunk_key("");
33992 let snapshot = editor.buffer().read(cx).snapshot(cx);
33993 let comments = editor.comments_for_hunk(&key, &snapshot);
33994 assert_eq!(comments.len(), 1);
33995 assert!(comments[0].is_editing);
33996 })
33997 .unwrap();
33998
33999 // Update the comment
34000 editor
34001 .update(cx, |editor, _window, cx| {
34002 let updated =
34003 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
34004 assert!(updated);
34005 })
34006 .unwrap();
34007
34008 // Verify comment was updated and editing flag is cleared
34009 editor
34010 .update(cx, |editor, _window, cx| {
34011 let key = test_hunk_key("");
34012 let snapshot = editor.buffer().read(cx).snapshot(cx);
34013 let comments = editor.comments_for_hunk(&key, &snapshot);
34014 assert_eq!(comments[0].comment, "Updated comment");
34015 assert!(!comments[0].is_editing);
34016 })
34017 .unwrap();
34018}
34019
34020#[gpui::test]
34021fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
34022 init_test(cx, |_| {});
34023
34024 // Create an editor with some text
34025 let editor = cx.add_window(|window, cx| {
34026 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
34027 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34028 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34029 });
34030
34031 // Add a comment with an anchor on line 2
34032 editor
34033 .update(cx, |editor, _window, cx| {
34034 let snapshot = editor.buffer().read(cx).snapshot(cx);
34035 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
34036 let key = DiffHunkKey {
34037 file_path: Arc::from(util::rel_path::RelPath::empty()),
34038 hunk_start_anchor: anchor,
34039 };
34040 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
34041 assert_eq!(editor.total_review_comment_count(), 1);
34042 })
34043 .unwrap();
34044
34045 // Delete all content (this should orphan the comment's anchor)
34046 editor
34047 .update(cx, |editor, window, cx| {
34048 editor.select_all(&SelectAll, window, cx);
34049 editor.insert("completely new content", window, cx);
34050 })
34051 .unwrap();
34052
34053 // Trigger cleanup
34054 editor
34055 .update(cx, |editor, _window, cx| {
34056 editor.cleanup_orphaned_review_comments(cx);
34057 // Comment should be removed because its anchor is invalid
34058 assert_eq!(editor.total_review_comment_count(), 0);
34059 })
34060 .unwrap();
34061}
34062
34063#[gpui::test]
34064fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
34065 init_test(cx, |_| {});
34066
34067 // Create an editor with some text
34068 let editor = cx.add_window(|window, cx| {
34069 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
34070 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34071 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34072 });
34073
34074 // Add a comment with an anchor on line 2
34075 editor
34076 .update(cx, |editor, _window, cx| {
34077 let snapshot = editor.buffer().read(cx).snapshot(cx);
34078 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
34079 let key = DiffHunkKey {
34080 file_path: Arc::from(util::rel_path::RelPath::empty()),
34081 hunk_start_anchor: anchor,
34082 };
34083 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
34084 assert_eq!(editor.total_review_comment_count(), 1);
34085 })
34086 .unwrap();
34087
34088 // Edit the buffer - this should trigger cleanup via on_buffer_event
34089 // Delete all content which orphans the anchor
34090 editor
34091 .update(cx, |editor, window, cx| {
34092 editor.select_all(&SelectAll, window, cx);
34093 editor.insert("completely new content", window, cx);
34094 // The cleanup is called automatically in on_buffer_event when Edited fires
34095 })
34096 .unwrap();
34097
34098 // Verify cleanup happened automatically (not manually triggered)
34099 editor
34100 .update(cx, |editor, _window, _cx| {
34101 // Comment should be removed because its anchor became invalid
34102 // and cleanup was called automatically on buffer edit
34103 assert_eq!(editor.total_review_comment_count(), 0);
34104 })
34105 .unwrap();
34106}
34107
34108#[gpui::test]
34109fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
34110 init_test(cx, |_| {});
34111
34112 // This test verifies that comments can be stored for multiple different hunks
34113 // and that hunk_comment_count correctly identifies comments per hunk.
34114 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34115
34116 _ = editor.update(cx, |editor, _window, cx| {
34117 let snapshot = editor.buffer().read(cx).snapshot(cx);
34118
34119 // Create two different hunk keys (simulating two different files)
34120 let anchor = snapshot.anchor_before(Point::new(0, 0));
34121 let key1 = DiffHunkKey {
34122 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
34123 hunk_start_anchor: anchor,
34124 };
34125 let key2 = DiffHunkKey {
34126 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
34127 hunk_start_anchor: anchor,
34128 };
34129
34130 // Add comments to first hunk
34131 editor.add_review_comment(
34132 key1.clone(),
34133 "Comment 1 for file1".to_string(),
34134 anchor..anchor,
34135 cx,
34136 );
34137 editor.add_review_comment(
34138 key1.clone(),
34139 "Comment 2 for file1".to_string(),
34140 anchor..anchor,
34141 cx,
34142 );
34143
34144 // Add comment to second hunk
34145 editor.add_review_comment(
34146 key2.clone(),
34147 "Comment for file2".to_string(),
34148 anchor..anchor,
34149 cx,
34150 );
34151
34152 // Verify total count
34153 assert_eq!(editor.total_review_comment_count(), 3);
34154
34155 // Verify per-hunk counts
34156 let snapshot = editor.buffer().read(cx).snapshot(cx);
34157 assert_eq!(
34158 editor.hunk_comment_count(&key1, &snapshot),
34159 2,
34160 "file1 should have 2 comments"
34161 );
34162 assert_eq!(
34163 editor.hunk_comment_count(&key2, &snapshot),
34164 1,
34165 "file2 should have 1 comment"
34166 );
34167
34168 // Verify comments_for_hunk returns correct comments
34169 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
34170 assert_eq!(file1_comments.len(), 2);
34171 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
34172 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
34173
34174 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
34175 assert_eq!(file2_comments.len(), 1);
34176 assert_eq!(file2_comments[0].comment, "Comment for file2");
34177 });
34178}
34179
34180#[gpui::test]
34181fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
34182 init_test(cx, |_| {});
34183
34184 // This test verifies that hunk_keys_match correctly identifies when two
34185 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
34186 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34187
34188 _ = editor.update(cx, |editor, _window, cx| {
34189 let snapshot = editor.buffer().read(cx).snapshot(cx);
34190 let anchor = snapshot.anchor_before(Point::new(0, 0));
34191
34192 // Create two keys with the same file path and anchor
34193 let key1 = DiffHunkKey {
34194 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
34195 hunk_start_anchor: anchor,
34196 };
34197 let key2 = DiffHunkKey {
34198 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
34199 hunk_start_anchor: anchor,
34200 };
34201
34202 // Add comment to first key
34203 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
34204
34205 // Verify second key (same hunk) finds the comment
34206 let snapshot = editor.buffer().read(cx).snapshot(cx);
34207 assert_eq!(
34208 editor.hunk_comment_count(&key2, &snapshot),
34209 1,
34210 "Same hunk should find the comment"
34211 );
34212
34213 // Create a key with different file path
34214 let different_file_key = DiffHunkKey {
34215 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
34216 hunk_start_anchor: anchor,
34217 };
34218
34219 // Different file should not find the comment
34220 assert_eq!(
34221 editor.hunk_comment_count(&different_file_key, &snapshot),
34222 0,
34223 "Different file should not find the comment"
34224 );
34225 });
34226}
34227
34228#[gpui::test]
34229fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
34230 init_test(cx, |_| {});
34231
34232 // This test verifies that set_diff_review_comments_expanded correctly
34233 // updates the expanded state of overlays.
34234 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34235
34236 // Show overlay
34237 editor
34238 .update(cx, |editor, window, cx| {
34239 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
34240 })
34241 .unwrap();
34242
34243 // Verify initially expanded (default)
34244 editor
34245 .update(cx, |editor, _window, _cx| {
34246 assert!(
34247 editor.diff_review_overlays[0].comments_expanded,
34248 "Should be expanded by default"
34249 );
34250 })
34251 .unwrap();
34252
34253 // Set to collapsed using the public method
34254 editor
34255 .update(cx, |editor, _window, cx| {
34256 editor.set_diff_review_comments_expanded(false, cx);
34257 })
34258 .unwrap();
34259
34260 // Verify collapsed
34261 editor
34262 .update(cx, |editor, _window, _cx| {
34263 assert!(
34264 !editor.diff_review_overlays[0].comments_expanded,
34265 "Should be collapsed after setting to false"
34266 );
34267 })
34268 .unwrap();
34269
34270 // Set back to expanded
34271 editor
34272 .update(cx, |editor, _window, cx| {
34273 editor.set_diff_review_comments_expanded(true, cx);
34274 })
34275 .unwrap();
34276
34277 // Verify expanded again
34278 editor
34279 .update(cx, |editor, _window, _cx| {
34280 assert!(
34281 editor.diff_review_overlays[0].comments_expanded,
34282 "Should be expanded after setting to true"
34283 );
34284 })
34285 .unwrap();
34286}
34287
34288#[gpui::test]
34289fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
34290 init_test(cx, |_| {});
34291
34292 // Create an editor with multiple lines of text
34293 let editor = cx.add_window(|window, cx| {
34294 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
34295 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34296 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34297 });
34298
34299 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
34300 editor
34301 .update(cx, |editor, window, cx| {
34302 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
34303 })
34304 .unwrap();
34305
34306 // Verify line range
34307 editor
34308 .update(cx, |editor, _window, cx| {
34309 assert!(!editor.diff_review_overlays.is_empty());
34310 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
34311 })
34312 .unwrap();
34313
34314 // Dismiss and test with reversed range (end < start)
34315 editor
34316 .update(cx, |editor, _window, cx| {
34317 editor.dismiss_all_diff_review_overlays(cx);
34318 })
34319 .unwrap();
34320
34321 // Show overlay with reversed range - should normalize it
34322 editor
34323 .update(cx, |editor, window, cx| {
34324 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
34325 })
34326 .unwrap();
34327
34328 // Verify range is normalized (start <= end)
34329 editor
34330 .update(cx, |editor, _window, cx| {
34331 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
34332 })
34333 .unwrap();
34334}
34335
34336#[gpui::test]
34337fn test_diff_review_drag_state(cx: &mut TestAppContext) {
34338 init_test(cx, |_| {});
34339
34340 let editor = cx.add_window(|window, cx| {
34341 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
34342 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34343 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34344 });
34345
34346 // Initially no drag state
34347 editor
34348 .update(cx, |editor, _window, _cx| {
34349 assert!(editor.diff_review_drag_state.is_none());
34350 })
34351 .unwrap();
34352
34353 // Start drag at row 1
34354 editor
34355 .update(cx, |editor, window, cx| {
34356 editor.start_diff_review_drag(DisplayRow(1), window, cx);
34357 })
34358 .unwrap();
34359
34360 // Verify drag state is set
34361 editor
34362 .update(cx, |editor, window, cx| {
34363 assert!(editor.diff_review_drag_state.is_some());
34364 let snapshot = editor.snapshot(window, cx);
34365 let range = editor
34366 .diff_review_drag_state
34367 .as_ref()
34368 .unwrap()
34369 .row_range(&snapshot.display_snapshot);
34370 assert_eq!(*range.start(), DisplayRow(1));
34371 assert_eq!(*range.end(), DisplayRow(1));
34372 })
34373 .unwrap();
34374
34375 // Update drag to row 3
34376 editor
34377 .update(cx, |editor, window, cx| {
34378 editor.update_diff_review_drag(DisplayRow(3), window, cx);
34379 })
34380 .unwrap();
34381
34382 // Verify drag state is updated
34383 editor
34384 .update(cx, |editor, window, cx| {
34385 assert!(editor.diff_review_drag_state.is_some());
34386 let snapshot = editor.snapshot(window, cx);
34387 let range = editor
34388 .diff_review_drag_state
34389 .as_ref()
34390 .unwrap()
34391 .row_range(&snapshot.display_snapshot);
34392 assert_eq!(*range.start(), DisplayRow(1));
34393 assert_eq!(*range.end(), DisplayRow(3));
34394 })
34395 .unwrap();
34396
34397 // End drag - should show overlay
34398 editor
34399 .update(cx, |editor, window, cx| {
34400 editor.end_diff_review_drag(window, cx);
34401 })
34402 .unwrap();
34403
34404 // Verify drag state is cleared and overlay is shown
34405 editor
34406 .update(cx, |editor, _window, cx| {
34407 assert!(editor.diff_review_drag_state.is_none());
34408 assert!(!editor.diff_review_overlays.is_empty());
34409 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
34410 })
34411 .unwrap();
34412}
34413
34414#[gpui::test]
34415fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
34416 init_test(cx, |_| {});
34417
34418 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34419
34420 // Start drag
34421 editor
34422 .update(cx, |editor, window, cx| {
34423 editor.start_diff_review_drag(DisplayRow(0), window, cx);
34424 })
34425 .unwrap();
34426
34427 // Verify drag state is set
34428 editor
34429 .update(cx, |editor, _window, _cx| {
34430 assert!(editor.diff_review_drag_state.is_some());
34431 })
34432 .unwrap();
34433
34434 // Cancel drag
34435 editor
34436 .update(cx, |editor, _window, cx| {
34437 editor.cancel_diff_review_drag(cx);
34438 })
34439 .unwrap();
34440
34441 // Verify drag state is cleared and no overlay was created
34442 editor
34443 .update(cx, |editor, _window, _cx| {
34444 assert!(editor.diff_review_drag_state.is_none());
34445 assert!(editor.diff_review_overlays.is_empty());
34446 })
34447 .unwrap();
34448}
34449
34450#[gpui::test]
34451fn test_calculate_overlay_height(cx: &mut TestAppContext) {
34452 init_test(cx, |_| {});
34453
34454 // This test verifies that calculate_overlay_height returns correct heights
34455 // based on comment count and expanded state.
34456 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34457
34458 _ = editor.update(cx, |editor, _window, cx| {
34459 let snapshot = editor.buffer().read(cx).snapshot(cx);
34460 let anchor = snapshot.anchor_before(Point::new(0, 0));
34461 let key = DiffHunkKey {
34462 file_path: Arc::from(util::rel_path::RelPath::empty()),
34463 hunk_start_anchor: anchor,
34464 };
34465
34466 // No comments: base height of 2
34467 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
34468 assert_eq!(
34469 height_no_comments, 2,
34470 "Base height should be 2 with no comments"
34471 );
34472
34473 // Add one comment
34474 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
34475
34476 let snapshot = editor.buffer().read(cx).snapshot(cx);
34477
34478 // With comments expanded: base (2) + header (1) + 2 per comment
34479 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
34480 assert_eq!(
34481 height_expanded,
34482 2 + 1 + 2, // base + header + 1 comment * 2
34483 "Height with 1 comment expanded"
34484 );
34485
34486 // With comments collapsed: base (2) + header (1)
34487 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
34488 assert_eq!(
34489 height_collapsed,
34490 2 + 1, // base + header only
34491 "Height with comments collapsed"
34492 );
34493
34494 // Add more comments
34495 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
34496 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
34497
34498 let snapshot = editor.buffer().read(cx).snapshot(cx);
34499
34500 // With 3 comments expanded
34501 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
34502 assert_eq!(
34503 height_3_expanded,
34504 2 + 1 + (3 * 2), // base + header + 3 comments * 2
34505 "Height with 3 comments expanded"
34506 );
34507
34508 // Collapsed height stays the same regardless of comment count
34509 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
34510 assert_eq!(
34511 height_3_collapsed,
34512 2 + 1, // base + header only
34513 "Height with 3 comments collapsed should be same as 1 comment collapsed"
34514 );
34515 });
34516}
34517
34518#[gpui::test]
34519async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
34520 init_test(cx, |_| {});
34521
34522 let language = Arc::new(Language::new(
34523 LanguageConfig::default(),
34524 Some(tree_sitter_rust::LANGUAGE.into()),
34525 ));
34526
34527 let text = r#"
34528 fn main() {
34529 let x = foo(1, 2);
34530 }
34531 "#
34532 .unindent();
34533
34534 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34535 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34536 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34537
34538 editor
34539 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34540 .await;
34541
34542 // Test case 1: Move to end of syntax nodes
34543 editor.update_in(cx, |editor, window, cx| {
34544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34545 s.select_display_ranges([
34546 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
34547 ]);
34548 });
34549 });
34550 editor.update(cx, |editor, cx| {
34551 assert_text_with_selections(
34552 editor,
34553 indoc! {r#"
34554 fn main() {
34555 let x = foo(ˇ1, 2);
34556 }
34557 "#},
34558 cx,
34559 );
34560 });
34561 editor.update_in(cx, |editor, window, cx| {
34562 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34563 });
34564 editor.update(cx, |editor, cx| {
34565 assert_text_with_selections(
34566 editor,
34567 indoc! {r#"
34568 fn main() {
34569 let x = foo(1ˇ, 2);
34570 }
34571 "#},
34572 cx,
34573 );
34574 });
34575 editor.update_in(cx, |editor, window, cx| {
34576 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34577 });
34578 editor.update(cx, |editor, cx| {
34579 assert_text_with_selections(
34580 editor,
34581 indoc! {r#"
34582 fn main() {
34583 let x = foo(1, 2)ˇ;
34584 }
34585 "#},
34586 cx,
34587 );
34588 });
34589 editor.update_in(cx, |editor, window, cx| {
34590 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34591 });
34592 editor.update(cx, |editor, cx| {
34593 assert_text_with_selections(
34594 editor,
34595 indoc! {r#"
34596 fn main() {
34597 let x = foo(1, 2);ˇ
34598 }
34599 "#},
34600 cx,
34601 );
34602 });
34603 editor.update_in(cx, |editor, window, cx| {
34604 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34605 });
34606 editor.update(cx, |editor, cx| {
34607 assert_text_with_selections(
34608 editor,
34609 indoc! {r#"
34610 fn main() {
34611 let x = foo(1, 2);
34612 }ˇ
34613 "#},
34614 cx,
34615 );
34616 });
34617
34618 // Test case 2: Move to start of syntax nodes
34619 editor.update_in(cx, |editor, window, cx| {
34620 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34621 s.select_display_ranges([
34622 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
34623 ]);
34624 });
34625 });
34626 editor.update(cx, |editor, cx| {
34627 assert_text_with_selections(
34628 editor,
34629 indoc! {r#"
34630 fn main() {
34631 let x = foo(1, 2ˇ);
34632 }
34633 "#},
34634 cx,
34635 );
34636 });
34637 editor.update_in(cx, |editor, window, cx| {
34638 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34639 });
34640 editor.update(cx, |editor, cx| {
34641 assert_text_with_selections(
34642 editor,
34643 indoc! {r#"
34644 fn main() {
34645 let x = fooˇ(1, 2);
34646 }
34647 "#},
34648 cx,
34649 );
34650 });
34651 editor.update_in(cx, |editor, window, cx| {
34652 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34653 });
34654 editor.update(cx, |editor, cx| {
34655 assert_text_with_selections(
34656 editor,
34657 indoc! {r#"
34658 fn main() {
34659 let x = ˇfoo(1, 2);
34660 }
34661 "#},
34662 cx,
34663 );
34664 });
34665 editor.update_in(cx, |editor, window, cx| {
34666 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34667 });
34668 editor.update(cx, |editor, cx| {
34669 assert_text_with_selections(
34670 editor,
34671 indoc! {r#"
34672 fn main() {
34673 ˇlet x = foo(1, 2);
34674 }
34675 "#},
34676 cx,
34677 );
34678 });
34679 editor.update_in(cx, |editor, window, cx| {
34680 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34681 });
34682 editor.update(cx, |editor, cx| {
34683 assert_text_with_selections(
34684 editor,
34685 indoc! {r#"
34686 fn main() ˇ{
34687 let x = foo(1, 2);
34688 }
34689 "#},
34690 cx,
34691 );
34692 });
34693 editor.update_in(cx, |editor, window, cx| {
34694 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34695 });
34696 editor.update(cx, |editor, cx| {
34697 assert_text_with_selections(
34698 editor,
34699 indoc! {r#"
34700 ˇfn main() {
34701 let x = foo(1, 2);
34702 }
34703 "#},
34704 cx,
34705 );
34706 });
34707}
34708
34709#[gpui::test]
34710async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
34711 init_test(cx, |_| {});
34712
34713 let language = Arc::new(Language::new(
34714 LanguageConfig::default(),
34715 Some(tree_sitter_rust::LANGUAGE.into()),
34716 ));
34717
34718 let text = r#"
34719 fn main() {
34720 let x = foo(1, 2);
34721 let y = bar(3, 4);
34722 }
34723 "#
34724 .unindent();
34725
34726 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34727 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34728 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34729
34730 editor
34731 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34732 .await;
34733
34734 // Test case 1: Move to end of syntax nodes with two cursors
34735 editor.update_in(cx, |editor, window, cx| {
34736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34737 s.select_display_ranges([
34738 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
34739 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
34740 ]);
34741 });
34742 });
34743 editor.update(cx, |editor, cx| {
34744 assert_text_with_selections(
34745 editor,
34746 indoc! {r#"
34747 fn main() {
34748 let x = foo(1, 2ˇ);
34749 let y = bar(3, 4ˇ);
34750 }
34751 "#},
34752 cx,
34753 );
34754 });
34755 editor.update_in(cx, |editor, window, cx| {
34756 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34757 });
34758 editor.update(cx, |editor, cx| {
34759 assert_text_with_selections(
34760 editor,
34761 indoc! {r#"
34762 fn main() {
34763 let x = foo(1, 2)ˇ;
34764 let y = bar(3, 4)ˇ;
34765 }
34766 "#},
34767 cx,
34768 );
34769 });
34770 editor.update_in(cx, |editor, window, cx| {
34771 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34772 });
34773 editor.update(cx, |editor, cx| {
34774 assert_text_with_selections(
34775 editor,
34776 indoc! {r#"
34777 fn main() {
34778 let x = foo(1, 2);ˇ
34779 let y = bar(3, 4);ˇ
34780 }
34781 "#},
34782 cx,
34783 );
34784 });
34785
34786 // Test case 2: Move to start of syntax nodes with two cursors
34787 editor.update_in(cx, |editor, window, cx| {
34788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34789 s.select_display_ranges([
34790 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
34791 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
34792 ]);
34793 });
34794 });
34795 editor.update(cx, |editor, cx| {
34796 assert_text_with_selections(
34797 editor,
34798 indoc! {r#"
34799 fn main() {
34800 let x = foo(1, ˇ2);
34801 let y = bar(3, ˇ4);
34802 }
34803 "#},
34804 cx,
34805 );
34806 });
34807 editor.update_in(cx, |editor, window, cx| {
34808 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34809 });
34810 editor.update(cx, |editor, cx| {
34811 assert_text_with_selections(
34812 editor,
34813 indoc! {r#"
34814 fn main() {
34815 let x = fooˇ(1, 2);
34816 let y = barˇ(3, 4);
34817 }
34818 "#},
34819 cx,
34820 );
34821 });
34822 editor.update_in(cx, |editor, window, cx| {
34823 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34824 });
34825 editor.update(cx, |editor, cx| {
34826 assert_text_with_selections(
34827 editor,
34828 indoc! {r#"
34829 fn main() {
34830 let x = ˇfoo(1, 2);
34831 let y = ˇbar(3, 4);
34832 }
34833 "#},
34834 cx,
34835 );
34836 });
34837 editor.update_in(cx, |editor, window, cx| {
34838 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34839 });
34840 editor.update(cx, |editor, cx| {
34841 assert_text_with_selections(
34842 editor,
34843 indoc! {r#"
34844 fn main() {
34845 ˇlet x = foo(1, 2);
34846 ˇlet y = bar(3, 4);
34847 }
34848 "#},
34849 cx,
34850 );
34851 });
34852}
34853
34854#[gpui::test]
34855async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
34856 cx: &mut TestAppContext,
34857) {
34858 init_test(cx, |_| {});
34859
34860 let language = Arc::new(Language::new(
34861 LanguageConfig::default(),
34862 Some(tree_sitter_rust::LANGUAGE.into()),
34863 ));
34864
34865 let text = r#"
34866 fn main() {
34867 let x = foo(1, 2);
34868 let msg = "hello world";
34869 }
34870 "#
34871 .unindent();
34872
34873 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34874 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34875 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34876
34877 editor
34878 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34879 .await;
34880
34881 // Test case 1: With existing selection, move_to_end keeps selection
34882 editor.update_in(cx, |editor, window, cx| {
34883 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34884 s.select_display_ranges([
34885 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
34886 ]);
34887 });
34888 });
34889 editor.update(cx, |editor, cx| {
34890 assert_text_with_selections(
34891 editor,
34892 indoc! {r#"
34893 fn main() {
34894 let x = «foo(1, 2)ˇ»;
34895 let msg = "hello world";
34896 }
34897 "#},
34898 cx,
34899 );
34900 });
34901 editor.update_in(cx, |editor, window, cx| {
34902 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34903 });
34904 editor.update(cx, |editor, cx| {
34905 assert_text_with_selections(
34906 editor,
34907 indoc! {r#"
34908 fn main() {
34909 let x = «foo(1, 2)ˇ»;
34910 let msg = "hello world";
34911 }
34912 "#},
34913 cx,
34914 );
34915 });
34916
34917 // Test case 2: Move to end within a string
34918 editor.update_in(cx, |editor, window, cx| {
34919 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34920 s.select_display_ranges([
34921 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
34922 ]);
34923 });
34924 });
34925 editor.update(cx, |editor, cx| {
34926 assert_text_with_selections(
34927 editor,
34928 indoc! {r#"
34929 fn main() {
34930 let x = foo(1, 2);
34931 let msg = "ˇhello world";
34932 }
34933 "#},
34934 cx,
34935 );
34936 });
34937 editor.update_in(cx, |editor, window, cx| {
34938 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34939 });
34940 editor.update(cx, |editor, cx| {
34941 assert_text_with_selections(
34942 editor,
34943 indoc! {r#"
34944 fn main() {
34945 let x = foo(1, 2);
34946 let msg = "hello worldˇ";
34947 }
34948 "#},
34949 cx,
34950 );
34951 });
34952
34953 // Test case 3: Move to start within a string
34954 editor.update_in(cx, |editor, window, cx| {
34955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34956 s.select_display_ranges([
34957 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
34958 ]);
34959 });
34960 });
34961 editor.update(cx, |editor, cx| {
34962 assert_text_with_selections(
34963 editor,
34964 indoc! {r#"
34965 fn main() {
34966 let x = foo(1, 2);
34967 let msg = "hello ˇworld";
34968 }
34969 "#},
34970 cx,
34971 );
34972 });
34973 editor.update_in(cx, |editor, window, cx| {
34974 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34975 });
34976 editor.update(cx, |editor, cx| {
34977 assert_text_with_selections(
34978 editor,
34979 indoc! {r#"
34980 fn main() {
34981 let x = foo(1, 2);
34982 let msg = "ˇhello world";
34983 }
34984 "#},
34985 cx,
34986 );
34987 });
34988}
34989
34990#[gpui::test]
34991async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
34992 init_test(cx, |_| {});
34993
34994 let language = Arc::new(Language::new(
34995 LanguageConfig::default(),
34996 Some(tree_sitter_rust::LANGUAGE.into()),
34997 ));
34998
34999 // Test Group 1.1: Cursor in String - First Jump (Select to End)
35000 let text = r#"let msg = "foo bar baz";"#.unindent();
35001
35002 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35003 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35004 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35005
35006 editor
35007 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35008 .await;
35009
35010 editor.update_in(cx, |editor, window, cx| {
35011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35012 s.select_display_ranges([
35013 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
35014 ]);
35015 });
35016 });
35017 editor.update(cx, |editor, cx| {
35018 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
35019 });
35020 editor.update_in(cx, |editor, window, cx| {
35021 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35022 });
35023 editor.update(cx, |editor, cx| {
35024 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
35025 });
35026
35027 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
35028 editor.update_in(cx, |editor, window, cx| {
35029 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35030 });
35031 editor.update(cx, |editor, cx| {
35032 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
35033 });
35034
35035 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
35036 editor.update_in(cx, |editor, window, cx| {
35037 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35038 });
35039 editor.update(cx, |editor, cx| {
35040 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
35041 });
35042
35043 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
35044 editor.update_in(cx, |editor, window, cx| {
35045 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35046 s.select_display_ranges([
35047 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
35048 ]);
35049 });
35050 });
35051 editor.update(cx, |editor, cx| {
35052 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
35053 });
35054 editor.update_in(cx, |editor, window, cx| {
35055 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35056 });
35057 editor.update(cx, |editor, cx| {
35058 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
35059 });
35060
35061 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
35062 editor.update_in(cx, |editor, window, cx| {
35063 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35064 });
35065 editor.update(cx, |editor, cx| {
35066 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
35067 });
35068
35069 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
35070 editor.update_in(cx, |editor, window, cx| {
35071 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35072 });
35073 editor.update(cx, |editor, cx| {
35074 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
35075 });
35076
35077 // Test Group 2.1: Let Statement Progression (Select to End)
35078 let text = r#"
35079fn main() {
35080 let x = "hello";
35081}
35082"#
35083 .unindent();
35084
35085 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35086 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35087 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35088
35089 editor
35090 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35091 .await;
35092
35093 editor.update_in(cx, |editor, window, cx| {
35094 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35095 s.select_display_ranges([
35096 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
35097 ]);
35098 });
35099 });
35100 editor.update(cx, |editor, cx| {
35101 assert_text_with_selections(
35102 editor,
35103 indoc! {r#"
35104 fn main() {
35105 let xˇ = "hello";
35106 }
35107 "#},
35108 cx,
35109 );
35110 });
35111 editor.update_in(cx, |editor, window, cx| {
35112 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35113 });
35114 editor.update(cx, |editor, cx| {
35115 assert_text_with_selections(
35116 editor,
35117 indoc! {r##"
35118 fn main() {
35119 let x« = "hello";ˇ»
35120 }
35121 "##},
35122 cx,
35123 );
35124 });
35125 editor.update_in(cx, |editor, window, cx| {
35126 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35127 });
35128 editor.update(cx, |editor, cx| {
35129 assert_text_with_selections(
35130 editor,
35131 indoc! {r#"
35132 fn main() {
35133 let x« = "hello";
35134 }ˇ»
35135 "#},
35136 cx,
35137 );
35138 });
35139
35140 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
35141 let text = r#"let x = "hello";"#.unindent();
35142
35143 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35144 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35145 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35146
35147 editor
35148 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35149 .await;
35150
35151 editor.update_in(cx, |editor, window, cx| {
35152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35153 s.select_display_ranges([
35154 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
35155 ]);
35156 });
35157 });
35158 editor.update(cx, |editor, cx| {
35159 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
35160 });
35161 editor.update_in(cx, |editor, window, cx| {
35162 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35163 });
35164 editor.update(cx, |editor, cx| {
35165 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
35166 });
35167
35168 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
35169 editor.update_in(cx, |editor, window, cx| {
35170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35171 s.select_display_ranges([
35172 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
35173 ]);
35174 });
35175 });
35176 editor.update(cx, |editor, cx| {
35177 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
35178 });
35179 editor.update_in(cx, |editor, window, cx| {
35180 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35181 });
35182 editor.update(cx, |editor, cx| {
35183 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
35184 });
35185
35186 // Test Group 3.1: Create Selection from Cursor (Select to End)
35187 let text = r#"let x = "hello world";"#.unindent();
35188
35189 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35190 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35191 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35192
35193 editor
35194 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35195 .await;
35196
35197 editor.update_in(cx, |editor, window, cx| {
35198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35199 s.select_display_ranges([
35200 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
35201 ]);
35202 });
35203 });
35204 editor.update(cx, |editor, cx| {
35205 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
35206 });
35207 editor.update_in(cx, |editor, window, cx| {
35208 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35209 });
35210 editor.update(cx, |editor, cx| {
35211 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
35212 });
35213
35214 // Test Group 3.2: Extend Existing Selection (Select to End)
35215 editor.update_in(cx, |editor, window, cx| {
35216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35217 s.select_display_ranges([
35218 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
35219 ]);
35220 });
35221 });
35222 editor.update(cx, |editor, cx| {
35223 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
35224 });
35225 editor.update_in(cx, |editor, window, cx| {
35226 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35227 });
35228 editor.update(cx, |editor, cx| {
35229 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
35230 });
35231
35232 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
35233 let text = r#"let x = "hello"; let y = 42;"#.unindent();
35234
35235 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35236 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35237 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35238
35239 editor
35240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35241 .await;
35242
35243 editor.update_in(cx, |editor, window, cx| {
35244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35245 s.select_display_ranges([
35246 // Cursor inside string content
35247 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
35248 // Cursor at let statement semicolon
35249 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
35250 // Cursor inside integer literal
35251 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
35252 ]);
35253 });
35254 });
35255 editor.update(cx, |editor, cx| {
35256 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
35257 });
35258 editor.update_in(cx, |editor, window, cx| {
35259 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35260 });
35261 editor.update(cx, |editor, cx| {
35262 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
35263 });
35264
35265 // Test Group 4.2: Multiple Cursors on Separate Lines
35266 let text = r#"
35267let x = "hello";
35268let y = 42;
35269"#
35270 .unindent();
35271
35272 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35274 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35275
35276 editor
35277 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35278 .await;
35279
35280 editor.update_in(cx, |editor, window, cx| {
35281 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35282 s.select_display_ranges([
35283 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
35284 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
35285 ]);
35286 });
35287 });
35288
35289 editor.update(cx, |editor, cx| {
35290 assert_text_with_selections(
35291 editor,
35292 indoc! {r#"
35293 let x = "helˇlo";
35294 let y = 4ˇ2;
35295 "#},
35296 cx,
35297 );
35298 });
35299 editor.update_in(cx, |editor, window, cx| {
35300 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35301 });
35302 editor.update(cx, |editor, cx| {
35303 assert_text_with_selections(
35304 editor,
35305 indoc! {r#"
35306 let x = "hel«loˇ»";
35307 let y = 4«2ˇ»;
35308 "#},
35309 cx,
35310 );
35311 });
35312
35313 // Test Group 5.1: Nested Function Calls
35314 let text = r#"let result = foo(bar("arg"));"#.unindent();
35315
35316 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35317 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35318 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35319
35320 editor
35321 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35322 .await;
35323
35324 editor.update_in(cx, |editor, window, cx| {
35325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35326 s.select_display_ranges([
35327 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
35328 ]);
35329 });
35330 });
35331 editor.update(cx, |editor, cx| {
35332 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
35333 });
35334 editor.update_in(cx, |editor, window, cx| {
35335 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35336 });
35337 editor.update(cx, |editor, cx| {
35338 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
35339 });
35340 editor.update_in(cx, |editor, window, cx| {
35341 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35342 });
35343 editor.update(cx, |editor, cx| {
35344 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
35345 });
35346 editor.update_in(cx, |editor, window, cx| {
35347 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35348 });
35349 editor.update(cx, |editor, cx| {
35350 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
35351 });
35352
35353 // Test Group 6.1: Block Comments
35354 let text = r#"let x = /* multi
35355 line
35356 comment */;"#
35357 .unindent();
35358
35359 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35360 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35361 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35362
35363 editor
35364 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35365 .await;
35366
35367 editor.update_in(cx, |editor, window, cx| {
35368 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35369 s.select_display_ranges([
35370 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
35371 ]);
35372 });
35373 });
35374 editor.update(cx, |editor, cx| {
35375 assert_text_with_selections(
35376 editor,
35377 indoc! {r#"
35378let x = /* multiˇ
35379line
35380comment */;"#},
35381 cx,
35382 );
35383 });
35384 editor.update_in(cx, |editor, window, cx| {
35385 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35386 });
35387 editor.update(cx, |editor, cx| {
35388 assert_text_with_selections(
35389 editor,
35390 indoc! {r#"
35391let x = /* multi«
35392line
35393comment */ˇ»;"#},
35394 cx,
35395 );
35396 });
35397
35398 // Test Group 6.2: Array/Vector Literals
35399 let text = r#"let arr = [1, 2, 3];"#.unindent();
35400
35401 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35402 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35403 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35404
35405 editor
35406 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35407 .await;
35408
35409 editor.update_in(cx, |editor, window, cx| {
35410 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35411 s.select_display_ranges([
35412 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
35413 ]);
35414 });
35415 });
35416 editor.update(cx, |editor, cx| {
35417 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
35418 });
35419 editor.update_in(cx, |editor, window, cx| {
35420 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35421 });
35422 editor.update(cx, |editor, cx| {
35423 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
35424 });
35425 editor.update_in(cx, |editor, window, cx| {
35426 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35427 });
35428 editor.update(cx, |editor, cx| {
35429 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
35430 });
35431}
35432
35433#[gpui::test]
35434async fn test_restore_and_next(cx: &mut TestAppContext) {
35435 init_test(cx, |_| {});
35436 let mut cx = EditorTestContext::new(cx).await;
35437
35438 let diff_base = r#"
35439 one
35440 two
35441 three
35442 four
35443 five
35444 "#
35445 .unindent();
35446
35447 cx.set_state(
35448 &r#"
35449 ONE
35450 two
35451 ˇTHREE
35452 four
35453 FIVE
35454 "#
35455 .unindent(),
35456 );
35457 cx.set_head_text(&diff_base);
35458
35459 cx.update_editor(|editor, window, cx| {
35460 editor.set_expand_all_diff_hunks(cx);
35461 editor.restore_and_next(&Default::default(), window, cx);
35462 });
35463 cx.run_until_parked();
35464
35465 cx.assert_state_with_diff(
35466 r#"
35467 - one
35468 + ONE
35469 two
35470 three
35471 four
35472 - ˇfive
35473 + FIVE
35474 "#
35475 .unindent(),
35476 );
35477
35478 cx.update_editor(|editor, window, cx| {
35479 editor.restore_and_next(&Default::default(), window, cx);
35480 });
35481 cx.run_until_parked();
35482
35483 cx.assert_state_with_diff(
35484 r#"
35485 - one
35486 + ONE
35487 two
35488 three
35489 four
35490 ˇfive
35491 "#
35492 .unindent(),
35493 );
35494}
35495
35496#[gpui::test]
35497async fn test_restore_hunk_with_stale_base_text(cx: &mut TestAppContext) {
35498 // Regression test: prepare_restore_change must read base_text from the same
35499 // snapshot the hunk came from, not from the live BufferDiff entity. The live
35500 // entity's base_text may have already been updated asynchronously (e.g.
35501 // because git HEAD changed) while the MultiBufferSnapshot still holds the
35502 // old hunk byte ranges — using both together causes Rope::slice to panic
35503 // when the old range exceeds the new base text length.
35504 init_test(cx, |_| {});
35505 let mut cx = EditorTestContext::new(cx).await;
35506
35507 let long_base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n";
35508 cx.set_state("ˇONE\ntwo\nTHREE\nfour\nFIVE\nsix\nseven\neight\nnine\nten\n");
35509 cx.set_head_text(long_base_text);
35510
35511 let buffer_id = cx.update_buffer(|buffer, _| buffer.remote_id());
35512
35513 // Verify we have hunks from the initial diff.
35514 let has_hunks = cx.update_editor(|editor, window, cx| {
35515 let snapshot = editor.snapshot(window, cx);
35516 let hunks = snapshot
35517 .buffer_snapshot()
35518 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len());
35519 hunks.count() > 0
35520 });
35521 assert!(has_hunks, "should have diff hunks before restoring");
35522
35523 // Now trigger a git HEAD change to a much shorter base text.
35524 // After this, the live BufferDiff entity's base_text buffer will be
35525 // updated synchronously (inside set_snapshot_with_secondary_inner),
35526 // but DiffChanged is deferred until parsing_idle completes.
35527 // We step the executor tick-by-tick to find the window where the
35528 // live base_text is already short but the MultiBuffer snapshot is
35529 // still stale (old hunks + old base_text).
35530 let short_base_text = "short\n";
35531 let fs = cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
35532 let path = cx.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
35533 fs.set_head_for_repo(
35534 &Path::new(path!("/root")).join(".git"),
35535 &[(path.as_unix_str(), short_base_text.to_string())],
35536 "newcommit",
35537 );
35538
35539 // Step the executor tick-by-tick. At each step, check whether the
35540 // race condition exists: live BufferDiff has short base text but
35541 // the MultiBuffer snapshot still has old (long) hunks.
35542 let mut found_race = false;
35543 for _ in 0..200 {
35544 cx.executor().tick();
35545
35546 let race_exists = cx.update_editor(|editor, _window, cx| {
35547 let multi_buffer = editor.buffer().read(cx);
35548 let diff_entity = match multi_buffer.diff_for(buffer_id) {
35549 Some(d) => d,
35550 None => return false,
35551 };
35552 let live_base_len = diff_entity.read(cx).base_text(cx).len();
35553 let snapshot = multi_buffer.snapshot(cx);
35554 let snapshot_base_len = snapshot
35555 .diff_for_buffer_id(buffer_id)
35556 .map(|d| d.base_text().len());
35557 // Race: live base text is shorter than what the snapshot knows.
35558 live_base_len < long_base_text.len() && snapshot_base_len == Some(long_base_text.len())
35559 });
35560
35561 if race_exists {
35562 found_race = true;
35563 // The race window is open: the live entity has new (short) base
35564 // text but the MultiBuffer snapshot still has old hunks with byte
35565 // ranges computed against the old long base text. Attempt restore.
35566 // Without the fix, this panics with "cannot summarize past end of
35567 // rope". With the fix, it reads base_text from the stale snapshot
35568 // (consistent with the stale hunks) and succeeds.
35569 cx.update_editor(|editor, window, cx| {
35570 editor.select_all(&SelectAll, window, cx);
35571 editor.git_restore(&Default::default(), window, cx);
35572 });
35573 break;
35574 }
35575 }
35576
35577 assert!(
35578 found_race,
35579 "failed to observe the race condition between \
35580 live BufferDiff base_text and stale MultiBuffer snapshot; \
35581 the test may need adjustment if the async diff pipeline changed"
35582 );
35583}
35584
35585#[gpui::test]
35586async fn test_align_selections(cx: &mut TestAppContext) {
35587 init_test(cx, |_| {});
35588 let mut cx = EditorTestContext::new(cx).await;
35589
35590 // 1) one cursor, no action
35591 let before = " abc\n abc\nabc\n ˇabc";
35592 cx.set_state(before);
35593 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35594 cx.assert_editor_state(before);
35595
35596 // 2) multiple cursors at different rows
35597 let before = indoc!(
35598 r#"
35599 let aˇbc = 123;
35600 let xˇyz = 456;
35601 let fˇoo = 789;
35602 let bˇar = 0;
35603 "#
35604 );
35605 let after = indoc!(
35606 r#"
35607 let a ˇbc = 123;
35608 let x ˇyz = 456;
35609 let f ˇoo = 789;
35610 let bˇar = 0;
35611 "#
35612 );
35613 cx.set_state(before);
35614 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35615 cx.assert_editor_state(after);
35616
35617 // 3) multiple selections at different rows
35618 let before = indoc!(
35619 r#"
35620 let «ˇabc» = 123;
35621 let «ˇxyz» = 456;
35622 let «ˇfoo» = 789;
35623 let «ˇbar» = 0;
35624 "#
35625 );
35626 let after = indoc!(
35627 r#"
35628 let «ˇabc» = 123;
35629 let «ˇxyz» = 456;
35630 let «ˇfoo» = 789;
35631 let «ˇbar» = 0;
35632 "#
35633 );
35634 cx.set_state(before);
35635 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35636 cx.assert_editor_state(after);
35637
35638 // 4) multiple selections at different rows, inverted head
35639 let before = indoc!(
35640 r#"
35641 let «abcˇ» = 123;
35642 // comment
35643 let «xyzˇ» = 456;
35644 let «fooˇ» = 789;
35645 let «barˇ» = 0;
35646 "#
35647 );
35648 let after = indoc!(
35649 r#"
35650 let «abcˇ» = 123;
35651 // comment
35652 let «xyzˇ» = 456;
35653 let «fooˇ» = 789;
35654 let «barˇ» = 0;
35655 "#
35656 );
35657 cx.set_state(before);
35658 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35659 cx.assert_editor_state(after);
35660}
35661
35662#[gpui::test]
35663async fn test_align_selections_multicolumn(cx: &mut TestAppContext) {
35664 init_test(cx, |_| {});
35665 let mut cx = EditorTestContext::new(cx).await;
35666
35667 // 1) Multicolumn, one non affected editor row
35668 let before = indoc!(
35669 r#"
35670 name «|ˇ» age «|ˇ» height «|ˇ» note
35671 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
35672 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
35673 Anything that is not selected
35674 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
35675 "#
35676 );
35677 let after = indoc!(
35678 r#"
35679 name «|ˇ» age «|ˇ» height «|ˇ» note
35680 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
35681 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
35682 Anything that is not selected
35683 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
35684 "#
35685 );
35686 cx.set_state(before);
35687 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35688 cx.assert_editor_state(after);
35689
35690 // 2) not all alignment rows has the number of alignment columns
35691 let before = indoc!(
35692 r#"
35693 name «|ˇ» age «|ˇ» height
35694 Matthew «|ˇ» 7 «|ˇ» 2333
35695 Mike «|ˇ» 1234
35696 Miles «|ˇ» 88 «|ˇ» 99
35697 "#
35698 );
35699 let after = indoc!(
35700 r#"
35701 name «|ˇ» age «|ˇ» height
35702 Matthew «|ˇ» 7 «|ˇ» 2333
35703 Mike «|ˇ» 1234
35704 Miles «|ˇ» 88 «|ˇ» 99
35705 "#
35706 );
35707 cx.set_state(before);
35708 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35709 cx.assert_editor_state(after);
35710
35711 // 3) A aligned column shall stay aligned
35712 let before = indoc!(
35713 r#"
35714 $ ˇa ˇa
35715 $ ˇa ˇa
35716 $ ˇa ˇa
35717 $ ˇa ˇa
35718 "#
35719 );
35720 let after = indoc!(
35721 r#"
35722 $ ˇa ˇa
35723 $ ˇa ˇa
35724 $ ˇa ˇa
35725 $ ˇa ˇa
35726 "#
35727 );
35728 cx.set_state(before);
35729 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35730 cx.assert_editor_state(after);
35731}