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, sync::Arc};
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_syntax_highlighting(rust_lang(), &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_syntax_highlighting(rust_lang(), &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_syntax_highlighting(rust_lang(), &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_syntax_highlighting(rust_lang(), &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
19283#[gpui::test]
19284async fn test_following(cx: &mut TestAppContext) {
19285 init_test(cx, |_| {});
19286
19287 let fs = FakeFs::new(cx.executor());
19288 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
19289
19290 let buffer = project.update(cx, |project, cx| {
19291 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
19292 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
19293 });
19294 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
19295 let follower = cx.update(|cx| {
19296 cx.open_window(
19297 WindowOptions {
19298 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
19299 gpui::Point::new(px(0.), px(0.)),
19300 gpui::Point::new(px(10.), px(80.)),
19301 ))),
19302 ..Default::default()
19303 },
19304 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
19305 )
19306 .unwrap()
19307 });
19308
19309 let is_still_following = Rc::new(RefCell::new(true));
19310 let follower_edit_event_count = Rc::new(RefCell::new(0));
19311 let pending_update = Rc::new(RefCell::new(None));
19312 let leader_entity = leader.root(cx).unwrap();
19313 let follower_entity = follower.root(cx).unwrap();
19314 _ = follower.update(cx, {
19315 let update = pending_update.clone();
19316 let is_still_following = is_still_following.clone();
19317 let follower_edit_event_count = follower_edit_event_count.clone();
19318 |_, window, cx| {
19319 cx.subscribe_in(
19320 &leader_entity,
19321 window,
19322 move |_, leader, event, window, cx| {
19323 leader.update(cx, |leader, cx| {
19324 leader.add_event_to_update_proto(
19325 event,
19326 &mut update.borrow_mut(),
19327 window,
19328 cx,
19329 );
19330 });
19331 },
19332 )
19333 .detach();
19334
19335 cx.subscribe_in(
19336 &follower_entity,
19337 window,
19338 move |_, _, event: &EditorEvent, _window, _cx| {
19339 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
19340 *is_still_following.borrow_mut() = false;
19341 }
19342
19343 if let EditorEvent::BufferEdited = event {
19344 *follower_edit_event_count.borrow_mut() += 1;
19345 }
19346 },
19347 )
19348 .detach();
19349 }
19350 });
19351
19352 // Update the selections only
19353 _ = leader.update(cx, |leader, window, cx| {
19354 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19355 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
19356 });
19357 });
19358 follower
19359 .update(cx, |follower, window, cx| {
19360 follower.apply_update_proto(
19361 &project,
19362 pending_update.borrow_mut().take().unwrap(),
19363 window,
19364 cx,
19365 )
19366 })
19367 .unwrap()
19368 .await
19369 .unwrap();
19370 _ = follower.update(cx, |follower, _, cx| {
19371 assert_eq!(
19372 follower.selections.ranges(&follower.display_snapshot(cx)),
19373 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
19374 );
19375 });
19376 assert!(*is_still_following.borrow());
19377 assert_eq!(*follower_edit_event_count.borrow(), 0);
19378
19379 // Update the scroll position only
19380 _ = leader.update(cx, |leader, window, cx| {
19381 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
19382 });
19383 follower
19384 .update(cx, |follower, window, cx| {
19385 follower.apply_update_proto(
19386 &project,
19387 pending_update.borrow_mut().take().unwrap(),
19388 window,
19389 cx,
19390 )
19391 })
19392 .unwrap()
19393 .await
19394 .unwrap();
19395 assert_eq!(
19396 follower
19397 .update(cx, |follower, _, cx| follower.scroll_position(cx))
19398 .unwrap(),
19399 gpui::Point::new(1.5, 3.5)
19400 );
19401 assert!(*is_still_following.borrow());
19402 assert_eq!(*follower_edit_event_count.borrow(), 0);
19403
19404 // Update the selections and scroll position. The follower's scroll position is updated
19405 // via autoscroll, not via the leader's exact scroll position.
19406 _ = leader.update(cx, |leader, window, cx| {
19407 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19408 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
19409 });
19410 leader.request_autoscroll(Autoscroll::newest(), cx);
19411 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
19412 });
19413 follower
19414 .update(cx, |follower, window, cx| {
19415 follower.apply_update_proto(
19416 &project,
19417 pending_update.borrow_mut().take().unwrap(),
19418 window,
19419 cx,
19420 )
19421 })
19422 .unwrap()
19423 .await
19424 .unwrap();
19425 _ = follower.update(cx, |follower, _, cx| {
19426 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
19427 assert_eq!(
19428 follower.selections.ranges(&follower.display_snapshot(cx)),
19429 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
19430 );
19431 });
19432 assert!(*is_still_following.borrow());
19433
19434 // Creating a pending selection that precedes another selection
19435 _ = leader.update(cx, |leader, window, cx| {
19436 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19437 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
19438 });
19439 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, 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!(
19455 follower.selections.ranges(&follower.display_snapshot(cx)),
19456 vec![
19457 MultiBufferOffset(0)..MultiBufferOffset(0),
19458 MultiBufferOffset(1)..MultiBufferOffset(1)
19459 ]
19460 );
19461 });
19462 assert!(*is_still_following.borrow());
19463
19464 // Extend the pending selection so that it surrounds another selection
19465 _ = leader.update(cx, |leader, window, cx| {
19466 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
19467 });
19468 follower
19469 .update(cx, |follower, window, cx| {
19470 follower.apply_update_proto(
19471 &project,
19472 pending_update.borrow_mut().take().unwrap(),
19473 window,
19474 cx,
19475 )
19476 })
19477 .unwrap()
19478 .await
19479 .unwrap();
19480 _ = follower.update(cx, |follower, _, cx| {
19481 assert_eq!(
19482 follower.selections.ranges(&follower.display_snapshot(cx)),
19483 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
19484 );
19485 });
19486
19487 // Scrolling locally breaks the follow
19488 _ = follower.update(cx, |follower, window, cx| {
19489 let top_anchor = follower
19490 .buffer()
19491 .read(cx)
19492 .read(cx)
19493 .anchor_after(MultiBufferOffset(0));
19494 follower.set_scroll_anchor(
19495 ScrollAnchor {
19496 anchor: top_anchor,
19497 offset: gpui::Point::new(0.0, 0.5),
19498 },
19499 window,
19500 cx,
19501 );
19502 });
19503 assert!(!(*is_still_following.borrow()));
19504}
19505
19506#[gpui::test]
19507async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
19508 init_test(cx, |_| {});
19509
19510 let fs = FakeFs::new(cx.executor());
19511 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
19512 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19513 let workspace = window
19514 .read_with(cx, |mw, _| mw.workspace().clone())
19515 .unwrap();
19516 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
19517
19518 let cx = &mut VisualTestContext::from_window(*window, cx);
19519
19520 let leader = pane.update_in(cx, |_, window, cx| {
19521 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
19522 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
19523 });
19524
19525 // Start following the editor when it has no excerpts.
19526 let mut state_message =
19527 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
19528 let workspace_entity = workspace.clone();
19529 let follower_1 = cx
19530 .update_window(*window, |_, window, cx| {
19531 Editor::from_state_proto(
19532 workspace_entity,
19533 ViewId {
19534 creator: CollaboratorId::PeerId(PeerId::default()),
19535 id: 0,
19536 },
19537 &mut state_message,
19538 window,
19539 cx,
19540 )
19541 })
19542 .unwrap()
19543 .unwrap()
19544 .await
19545 .unwrap();
19546
19547 let update_message = Rc::new(RefCell::new(None));
19548 follower_1.update_in(cx, {
19549 let update = update_message.clone();
19550 |_, window, cx| {
19551 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
19552 leader.update(cx, |leader, cx| {
19553 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
19554 });
19555 })
19556 .detach();
19557 }
19558 });
19559
19560 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
19561 (
19562 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),
19563 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),
19564 )
19565 });
19566
19567 // Insert some excerpts.
19568 leader.update(cx, |leader, cx| {
19569 leader.buffer.update(cx, |multibuffer, cx| {
19570 multibuffer.set_excerpts_for_path(
19571 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
19572 buffer_1.clone(),
19573 vec![
19574 Point::row_range(0..3),
19575 Point::row_range(1..6),
19576 Point::row_range(12..15),
19577 ],
19578 0,
19579 cx,
19580 );
19581 multibuffer.set_excerpts_for_path(
19582 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
19583 buffer_2.clone(),
19584 vec![Point::row_range(0..6), Point::row_range(8..12)],
19585 0,
19586 cx,
19587 );
19588 });
19589 });
19590
19591 // Apply the update of adding the excerpts.
19592 follower_1
19593 .update_in(cx, |follower, window, cx| {
19594 follower.apply_update_proto(
19595 &project,
19596 update_message.borrow().clone().unwrap(),
19597 window,
19598 cx,
19599 )
19600 })
19601 .await
19602 .unwrap();
19603 assert_eq!(
19604 follower_1.update(cx, |editor, cx| editor.text(cx)),
19605 leader.update(cx, |editor, cx| editor.text(cx))
19606 );
19607 update_message.borrow_mut().take();
19608
19609 // Start following separately after it already has excerpts.
19610 let mut state_message =
19611 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
19612 let workspace_entity = workspace.clone();
19613 let follower_2 = cx
19614 .update_window(*window, |_, window, cx| {
19615 Editor::from_state_proto(
19616 workspace_entity,
19617 ViewId {
19618 creator: CollaboratorId::PeerId(PeerId::default()),
19619 id: 0,
19620 },
19621 &mut state_message,
19622 window,
19623 cx,
19624 )
19625 })
19626 .unwrap()
19627 .unwrap()
19628 .await
19629 .unwrap();
19630 assert_eq!(
19631 follower_2.update(cx, |editor, cx| editor.text(cx)),
19632 leader.update(cx, |editor, cx| editor.text(cx))
19633 );
19634
19635 // Remove some excerpts.
19636 leader.update(cx, |leader, cx| {
19637 leader.buffer.update(cx, |multibuffer, cx| {
19638 multibuffer.remove_excerpts(
19639 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
19640 cx,
19641 );
19642 });
19643 });
19644
19645 // Apply the update of removing the excerpts.
19646 follower_1
19647 .update_in(cx, |follower, window, cx| {
19648 follower.apply_update_proto(
19649 &project,
19650 update_message.borrow().clone().unwrap(),
19651 window,
19652 cx,
19653 )
19654 })
19655 .await
19656 .unwrap();
19657 follower_2
19658 .update_in(cx, |follower, window, cx| {
19659 follower.apply_update_proto(
19660 &project,
19661 update_message.borrow().clone().unwrap(),
19662 window,
19663 cx,
19664 )
19665 })
19666 .await
19667 .unwrap();
19668 update_message.borrow_mut().take();
19669 assert_eq!(
19670 follower_1.update(cx, |editor, cx| editor.text(cx)),
19671 leader.update(cx, |editor, cx| editor.text(cx))
19672 );
19673}
19674
19675#[gpui::test]
19676async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19677 init_test(cx, |_| {});
19678
19679 let mut cx = EditorTestContext::new(cx).await;
19680 let lsp_store =
19681 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
19682
19683 cx.set_state(indoc! {"
19684 ˇfn func(abc def: i32) -> u32 {
19685 }
19686 "});
19687
19688 cx.update(|_, cx| {
19689 lsp_store.update(cx, |lsp_store, cx| {
19690 lsp_store
19691 .update_diagnostics(
19692 LanguageServerId(0),
19693 lsp::PublishDiagnosticsParams {
19694 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
19695 version: None,
19696 diagnostics: vec![
19697 lsp::Diagnostic {
19698 range: lsp::Range::new(
19699 lsp::Position::new(0, 11),
19700 lsp::Position::new(0, 12),
19701 ),
19702 severity: Some(lsp::DiagnosticSeverity::ERROR),
19703 ..Default::default()
19704 },
19705 lsp::Diagnostic {
19706 range: lsp::Range::new(
19707 lsp::Position::new(0, 12),
19708 lsp::Position::new(0, 15),
19709 ),
19710 severity: Some(lsp::DiagnosticSeverity::ERROR),
19711 ..Default::default()
19712 },
19713 lsp::Diagnostic {
19714 range: lsp::Range::new(
19715 lsp::Position::new(0, 25),
19716 lsp::Position::new(0, 28),
19717 ),
19718 severity: Some(lsp::DiagnosticSeverity::ERROR),
19719 ..Default::default()
19720 },
19721 ],
19722 },
19723 None,
19724 DiagnosticSourceKind::Pushed,
19725 &[],
19726 cx,
19727 )
19728 .unwrap()
19729 });
19730 });
19731
19732 executor.run_until_parked();
19733
19734 cx.update_editor(|editor, window, cx| {
19735 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19736 });
19737
19738 cx.assert_editor_state(indoc! {"
19739 fn func(abc def: i32) -> ˇu32 {
19740 }
19741 "});
19742
19743 cx.update_editor(|editor, window, cx| {
19744 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19745 });
19746
19747 cx.assert_editor_state(indoc! {"
19748 fn func(abc ˇdef: i32) -> u32 {
19749 }
19750 "});
19751
19752 cx.update_editor(|editor, window, cx| {
19753 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19754 });
19755
19756 cx.assert_editor_state(indoc! {"
19757 fn func(abcˇ def: i32) -> u32 {
19758 }
19759 "});
19760
19761 cx.update_editor(|editor, window, cx| {
19762 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19763 });
19764
19765 cx.assert_editor_state(indoc! {"
19766 fn func(abc def: i32) -> ˇu32 {
19767 }
19768 "});
19769}
19770
19771#[gpui::test]
19772async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19773 init_test(cx, |_| {});
19774
19775 let mut cx = EditorTestContext::new(cx).await;
19776
19777 let diff_base = r#"
19778 use some::mod;
19779
19780 const A: u32 = 42;
19781
19782 fn main() {
19783 println!("hello");
19784
19785 println!("world");
19786 }
19787 "#
19788 .unindent();
19789
19790 // Edits are modified, removed, modified, added
19791 cx.set_state(
19792 &r#"
19793 use some::modified;
19794
19795 ˇ
19796 fn main() {
19797 println!("hello there");
19798
19799 println!("around the");
19800 println!("world");
19801 }
19802 "#
19803 .unindent(),
19804 );
19805
19806 cx.set_head_text(&diff_base);
19807 executor.run_until_parked();
19808
19809 cx.update_editor(|editor, window, cx| {
19810 //Wrap around the bottom of the buffer
19811 for _ in 0..3 {
19812 editor.go_to_next_hunk(&GoToHunk, window, cx);
19813 }
19814 });
19815
19816 cx.assert_editor_state(
19817 &r#"
19818 ˇuse some::modified;
19819
19820
19821 fn main() {
19822 println!("hello there");
19823
19824 println!("around the");
19825 println!("world");
19826 }
19827 "#
19828 .unindent(),
19829 );
19830
19831 cx.update_editor(|editor, window, cx| {
19832 //Wrap around the top of the buffer
19833 for _ in 0..2 {
19834 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19835 }
19836 });
19837
19838 cx.assert_editor_state(
19839 &r#"
19840 use some::modified;
19841
19842
19843 fn main() {
19844 ˇ println!("hello there");
19845
19846 println!("around the");
19847 println!("world");
19848 }
19849 "#
19850 .unindent(),
19851 );
19852
19853 cx.update_editor(|editor, window, cx| {
19854 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19855 });
19856
19857 cx.assert_editor_state(
19858 &r#"
19859 use some::modified;
19860
19861 ˇ
19862 fn main() {
19863 println!("hello there");
19864
19865 println!("around the");
19866 println!("world");
19867 }
19868 "#
19869 .unindent(),
19870 );
19871
19872 cx.update_editor(|editor, window, cx| {
19873 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19874 });
19875
19876 cx.assert_editor_state(
19877 &r#"
19878 ˇuse some::modified;
19879
19880
19881 fn main() {
19882 println!("hello there");
19883
19884 println!("around the");
19885 println!("world");
19886 }
19887 "#
19888 .unindent(),
19889 );
19890
19891 cx.update_editor(|editor, window, cx| {
19892 for _ in 0..2 {
19893 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19894 }
19895 });
19896
19897 cx.assert_editor_state(
19898 &r#"
19899 use some::modified;
19900
19901
19902 fn main() {
19903 ˇ println!("hello there");
19904
19905 println!("around the");
19906 println!("world");
19907 }
19908 "#
19909 .unindent(),
19910 );
19911
19912 cx.update_editor(|editor, window, cx| {
19913 editor.fold(&Fold, window, cx);
19914 });
19915
19916 cx.update_editor(|editor, window, cx| {
19917 editor.go_to_next_hunk(&GoToHunk, window, cx);
19918 });
19919
19920 cx.assert_editor_state(
19921 &r#"
19922 ˇuse some::modified;
19923
19924
19925 fn main() {
19926 println!("hello there");
19927
19928 println!("around the");
19929 println!("world");
19930 }
19931 "#
19932 .unindent(),
19933 );
19934}
19935
19936#[test]
19937fn test_split_words() {
19938 fn split(text: &str) -> Vec<&str> {
19939 split_words(text).collect()
19940 }
19941
19942 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
19943 assert_eq!(split("hello_world"), &["hello_", "world"]);
19944 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
19945 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
19946 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
19947 assert_eq!(split("helloworld"), &["helloworld"]);
19948
19949 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
19950}
19951
19952#[test]
19953fn test_split_words_for_snippet_prefix() {
19954 fn split(text: &str) -> Vec<&str> {
19955 snippet_candidate_suffixes(text, &|c| c.is_alphanumeric() || c == '_').collect()
19956 }
19957
19958 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
19959 assert_eq!(split("hello_world"), &["hello_world"]);
19960 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
19961 assert_eq!(split("Hello_World"), &["Hello_World"]);
19962 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
19963 assert_eq!(split("helloworld"), &["helloworld"]);
19964 assert_eq!(
19965 split("this@is!@#$^many . symbols"),
19966 &[
19967 "symbols",
19968 " symbols",
19969 ". symbols",
19970 " . symbols",
19971 " . symbols",
19972 " . symbols",
19973 "many . symbols",
19974 "^many . symbols",
19975 "$^many . symbols",
19976 "#$^many . symbols",
19977 "@#$^many . symbols",
19978 "!@#$^many . symbols",
19979 "is!@#$^many . symbols",
19980 "@is!@#$^many . symbols",
19981 "this@is!@#$^many . symbols",
19982 ],
19983 );
19984 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
19985}
19986
19987#[gpui::test]
19988async fn test_move_to_syntax_node_relative_jumps(tcx: &mut TestAppContext) {
19989 init_test(tcx, |_| {});
19990
19991 let mut cx = EditorLspTestContext::new(
19992 Arc::into_inner(markdown_lang()).unwrap(),
19993 Default::default(),
19994 tcx,
19995 )
19996 .await;
19997
19998 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
19999 let _state_context = cx.set_state(before);
20000 cx.run_until_parked();
20001 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
20002 .await
20003 .unwrap();
20004 cx.run_until_parked();
20005 cx.assert_editor_state(after);
20006 }
20007
20008 const ABOVE: i8 = -1;
20009 const BELOW: i8 = 1;
20010
20011 assert(
20012 ABOVE,
20013 indoc! {"
20014 # Foo
20015
20016 ˇFoo foo foo
20017
20018 # Bar
20019
20020 Bar bar bar
20021 "},
20022 indoc! {"
20023 ˇ# Foo
20024
20025 Foo foo foo
20026
20027 # Bar
20028
20029 Bar bar bar
20030 "},
20031 &mut cx,
20032 )
20033 .await;
20034
20035 assert(
20036 ABOVE,
20037 indoc! {"
20038 ˇ# Foo
20039
20040 Foo foo foo
20041
20042 # Bar
20043
20044 Bar bar bar
20045 "},
20046 indoc! {"
20047 ˇ# Foo
20048
20049 Foo foo foo
20050
20051 # Bar
20052
20053 Bar bar bar
20054 "},
20055 &mut cx,
20056 )
20057 .await;
20058
20059 assert(
20060 BELOW,
20061 indoc! {"
20062 ˇ# Foo
20063
20064 Foo foo foo
20065
20066 # Bar
20067
20068 Bar bar bar
20069 "},
20070 indoc! {"
20071 # Foo
20072
20073 Foo foo foo
20074
20075 ˇ# Bar
20076
20077 Bar bar bar
20078 "},
20079 &mut cx,
20080 )
20081 .await;
20082
20083 assert(
20084 BELOW,
20085 indoc! {"
20086 # Foo
20087
20088 ˇFoo foo foo
20089
20090 # Bar
20091
20092 Bar bar bar
20093 "},
20094 indoc! {"
20095 # Foo
20096
20097 Foo foo foo
20098
20099 ˇ# Bar
20100
20101 Bar bar bar
20102 "},
20103 &mut cx,
20104 )
20105 .await;
20106
20107 assert(
20108 BELOW,
20109 indoc! {"
20110 # Foo
20111
20112 Foo foo foo
20113
20114 ˇ# Bar
20115
20116 Bar bar bar
20117 "},
20118 indoc! {"
20119 # Foo
20120
20121 Foo foo foo
20122
20123 ˇ# Bar
20124
20125 Bar bar bar
20126 "},
20127 &mut cx,
20128 )
20129 .await;
20130
20131 assert(
20132 BELOW,
20133 indoc! {"
20134 # Foo
20135
20136 Foo foo foo
20137
20138 # Bar
20139 ˇ
20140 Bar bar bar
20141 "},
20142 indoc! {"
20143 # Foo
20144
20145 Foo foo foo
20146
20147 # Bar
20148 ˇ
20149 Bar bar bar
20150 "},
20151 &mut cx,
20152 )
20153 .await;
20154}
20155
20156#[gpui::test]
20157async fn test_move_to_syntax_node_relative_dead_zone(tcx: &mut TestAppContext) {
20158 init_test(tcx, |_| {});
20159
20160 let mut cx = EditorLspTestContext::new(
20161 Arc::into_inner(rust_lang()).unwrap(),
20162 Default::default(),
20163 tcx,
20164 )
20165 .await;
20166
20167 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
20168 let _state_context = cx.set_state(before);
20169 cx.run_until_parked();
20170 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
20171 .await
20172 .unwrap();
20173 cx.run_until_parked();
20174 cx.assert_editor_state(after);
20175 }
20176
20177 const ABOVE: i8 = -1;
20178 const BELOW: i8 = 1;
20179
20180 assert(
20181 ABOVE,
20182 indoc! {"
20183 fn foo() {
20184 // foo fn
20185 }
20186
20187 ˇ// this zone is not inside any top level outline node
20188
20189 fn bar() {
20190 // bar fn
20191 let _ = 2;
20192 }
20193 "},
20194 indoc! {"
20195 ˇfn foo() {
20196 // foo fn
20197 }
20198
20199 // this zone is not inside any top level outline node
20200
20201 fn bar() {
20202 // bar fn
20203 let _ = 2;
20204 }
20205 "},
20206 &mut cx,
20207 )
20208 .await;
20209
20210 assert(
20211 BELOW,
20212 indoc! {"
20213 fn foo() {
20214 // foo fn
20215 }
20216
20217 ˇ// this zone is not inside any top level outline node
20218
20219 fn bar() {
20220 // bar fn
20221 let _ = 2;
20222 }
20223 "},
20224 indoc! {"
20225 fn foo() {
20226 // foo fn
20227 }
20228
20229 // this zone is not inside any top level outline node
20230
20231 ˇfn bar() {
20232 // bar fn
20233 let _ = 2;
20234 }
20235 "},
20236 &mut cx,
20237 )
20238 .await;
20239}
20240
20241#[gpui::test]
20242async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
20243 init_test(cx, |_| {});
20244
20245 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
20246
20247 #[track_caller]
20248 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
20249 let _state_context = cx.set_state(before);
20250 cx.run_until_parked();
20251 cx.update_editor(|editor, window, cx| {
20252 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
20253 });
20254 cx.run_until_parked();
20255 cx.assert_editor_state(after);
20256 }
20257
20258 // Outside bracket jumps to outside of matching bracket
20259 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
20260 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
20261
20262 // Inside bracket jumps to inside of matching bracket
20263 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
20264 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
20265
20266 // When outside a bracket and inside, favor jumping to the inside bracket
20267 assert(
20268 "console.log('foo', [1, 2, 3]ˇ);",
20269 "console.log('foo', ˇ[1, 2, 3]);",
20270 &mut cx,
20271 );
20272 assert(
20273 "console.log(ˇ'foo', [1, 2, 3]);",
20274 "console.log('foo'ˇ, [1, 2, 3]);",
20275 &mut cx,
20276 );
20277
20278 // Bias forward if two options are equally likely
20279 assert(
20280 "let result = curried_fun()ˇ();",
20281 "let result = curried_fun()()ˇ;",
20282 &mut cx,
20283 );
20284
20285 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
20286 assert(
20287 indoc! {"
20288 function test() {
20289 console.log('test')ˇ
20290 }"},
20291 indoc! {"
20292 function test() {
20293 console.logˇ('test')
20294 }"},
20295 &mut cx,
20296 );
20297}
20298
20299#[gpui::test]
20300async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
20301 init_test(cx, |_| {});
20302 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
20303 language_registry.add(markdown_lang());
20304 language_registry.add(rust_lang());
20305 let buffer = cx.new(|cx| {
20306 let mut buffer = language::Buffer::local(
20307 indoc! {"
20308 ```rs
20309 impl Worktree {
20310 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20311 }
20312 }
20313 ```
20314 "},
20315 cx,
20316 );
20317 buffer.set_language_registry(language_registry.clone());
20318 buffer.set_language(Some(markdown_lang()), cx);
20319 buffer
20320 });
20321 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20322 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
20323 cx.executor().run_until_parked();
20324 _ = editor.update(cx, |editor, window, cx| {
20325 // Case 1: Test outer enclosing brackets
20326 select_ranges(
20327 editor,
20328 &indoc! {"
20329 ```rs
20330 impl Worktree {
20331 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20332 }
20333 }ˇ
20334 ```
20335 "},
20336 window,
20337 cx,
20338 );
20339 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
20340 assert_text_with_selections(
20341 editor,
20342 &indoc! {"
20343 ```rs
20344 impl Worktree ˇ{
20345 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20346 }
20347 }
20348 ```
20349 "},
20350 cx,
20351 );
20352 // Case 2: Test inner enclosing brackets
20353 select_ranges(
20354 editor,
20355 &indoc! {"
20356 ```rs
20357 impl Worktree {
20358 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
20359 }ˇ
20360 }
20361 ```
20362 "},
20363 window,
20364 cx,
20365 );
20366 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
20367 assert_text_with_selections(
20368 editor,
20369 &indoc! {"
20370 ```rs
20371 impl Worktree {
20372 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
20373 }
20374 }
20375 ```
20376 "},
20377 cx,
20378 );
20379 });
20380}
20381
20382#[gpui::test]
20383async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
20384 init_test(cx, |_| {});
20385
20386 let fs = FakeFs::new(cx.executor());
20387 fs.insert_tree(
20388 path!("/a"),
20389 json!({
20390 "main.rs": "fn main() { let a = 5; }",
20391 "other.rs": "// Test file",
20392 }),
20393 )
20394 .await;
20395 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20396
20397 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20398 language_registry.add(Arc::new(Language::new(
20399 LanguageConfig {
20400 name: "Rust".into(),
20401 matcher: LanguageMatcher {
20402 path_suffixes: vec!["rs".to_string()],
20403 ..Default::default()
20404 },
20405 brackets: BracketPairConfig {
20406 pairs: vec![BracketPair {
20407 start: "{".to_string(),
20408 end: "}".to_string(),
20409 close: true,
20410 surround: true,
20411 newline: true,
20412 }],
20413 disabled_scopes_by_bracket_ix: Vec::new(),
20414 },
20415 ..Default::default()
20416 },
20417 Some(tree_sitter_rust::LANGUAGE.into()),
20418 )));
20419 let mut fake_servers = language_registry.register_fake_lsp(
20420 "Rust",
20421 FakeLspAdapter {
20422 capabilities: lsp::ServerCapabilities {
20423 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
20424 first_trigger_character: "{".to_string(),
20425 more_trigger_character: None,
20426 }),
20427 ..Default::default()
20428 },
20429 ..Default::default()
20430 },
20431 );
20432
20433 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
20434 let workspace = window
20435 .read_with(cx, |mw, _| mw.workspace().clone())
20436 .unwrap();
20437
20438 let cx = &mut VisualTestContext::from_window(*window, cx);
20439
20440 let worktree_id = workspace.update_in(cx, |workspace, _, cx| {
20441 workspace.project().update(cx, |project, cx| {
20442 project.worktrees(cx).next().unwrap().read(cx).id()
20443 })
20444 });
20445
20446 let buffer = project
20447 .update(cx, |project, cx| {
20448 project.open_local_buffer(path!("/a/main.rs"), cx)
20449 })
20450 .await
20451 .unwrap();
20452 let editor_handle = workspace
20453 .update_in(cx, |workspace, window, cx| {
20454 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
20455 })
20456 .await
20457 .unwrap()
20458 .downcast::<Editor>()
20459 .unwrap();
20460
20461 let fake_server = fake_servers.next().await.unwrap();
20462
20463 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
20464 |params, _| async move {
20465 assert_eq!(
20466 params.text_document_position.text_document.uri,
20467 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
20468 );
20469 assert_eq!(
20470 params.text_document_position.position,
20471 lsp::Position::new(0, 21),
20472 );
20473
20474 Ok(Some(vec![lsp::TextEdit {
20475 new_text: "]".to_string(),
20476 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20477 }]))
20478 },
20479 );
20480
20481 editor_handle.update_in(cx, |editor, window, cx| {
20482 window.focus(&editor.focus_handle(cx), cx);
20483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20484 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
20485 });
20486 editor.handle_input("{", window, cx);
20487 });
20488
20489 cx.executor().run_until_parked();
20490
20491 buffer.update(cx, |buffer, _| {
20492 assert_eq!(
20493 buffer.text(),
20494 "fn main() { let a = {5}; }",
20495 "No extra braces from on type formatting should appear in the buffer"
20496 )
20497 });
20498}
20499
20500#[gpui::test(iterations = 20, seeds(31))]
20501async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
20502 init_test(cx, |_| {});
20503
20504 let mut cx = EditorLspTestContext::new_rust(
20505 lsp::ServerCapabilities {
20506 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
20507 first_trigger_character: ".".to_string(),
20508 more_trigger_character: None,
20509 }),
20510 ..Default::default()
20511 },
20512 cx,
20513 )
20514 .await;
20515
20516 cx.update_buffer(|buffer, _| {
20517 // This causes autoindent to be async.
20518 buffer.set_sync_parse_timeout(None)
20519 });
20520
20521 cx.set_state("fn c() {\n d()ˇ\n}\n");
20522 cx.simulate_keystroke("\n");
20523 cx.run_until_parked();
20524
20525 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
20526 let mut request =
20527 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
20528 let buffer_cloned = buffer_cloned.clone();
20529 async move {
20530 buffer_cloned.update(&mut cx, |buffer, _| {
20531 assert_eq!(
20532 buffer.text(),
20533 "fn c() {\n d()\n .\n}\n",
20534 "OnTypeFormatting should triggered after autoindent applied"
20535 )
20536 });
20537
20538 Ok(Some(vec![]))
20539 }
20540 });
20541
20542 cx.simulate_keystroke(".");
20543 cx.run_until_parked();
20544
20545 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
20546 assert!(request.next().await.is_some());
20547 request.close();
20548 assert!(request.next().await.is_none());
20549}
20550
20551#[gpui::test]
20552async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
20553 init_test(cx, |_| {});
20554
20555 let fs = FakeFs::new(cx.executor());
20556 fs.insert_tree(
20557 path!("/a"),
20558 json!({
20559 "main.rs": "fn main() { let a = 5; }",
20560 "other.rs": "// Test file",
20561 }),
20562 )
20563 .await;
20564
20565 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20566
20567 let server_restarts = Arc::new(AtomicUsize::new(0));
20568 let closure_restarts = Arc::clone(&server_restarts);
20569 let language_server_name = "test language server";
20570 let language_name: LanguageName = "Rust".into();
20571
20572 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20573 language_registry.add(Arc::new(Language::new(
20574 LanguageConfig {
20575 name: language_name.clone(),
20576 matcher: LanguageMatcher {
20577 path_suffixes: vec!["rs".to_string()],
20578 ..Default::default()
20579 },
20580 ..Default::default()
20581 },
20582 Some(tree_sitter_rust::LANGUAGE.into()),
20583 )));
20584 let mut fake_servers = language_registry.register_fake_lsp(
20585 "Rust",
20586 FakeLspAdapter {
20587 name: language_server_name,
20588 initialization_options: Some(json!({
20589 "testOptionValue": true
20590 })),
20591 initializer: Some(Box::new(move |fake_server| {
20592 let task_restarts = Arc::clone(&closure_restarts);
20593 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
20594 task_restarts.fetch_add(1, atomic::Ordering::Release);
20595 futures::future::ready(Ok(()))
20596 });
20597 })),
20598 ..Default::default()
20599 },
20600 );
20601
20602 let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
20603 let _buffer = project
20604 .update(cx, |project, cx| {
20605 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
20606 })
20607 .await
20608 .unwrap();
20609 let _fake_server = fake_servers.next().await.unwrap();
20610 update_test_language_settings(cx, &|language_settings| {
20611 language_settings.languages.0.insert(
20612 language_name.clone().0.to_string(),
20613 LanguageSettingsContent {
20614 tab_size: NonZeroU32::new(8),
20615 ..Default::default()
20616 },
20617 );
20618 });
20619 cx.executor().run_until_parked();
20620 assert_eq!(
20621 server_restarts.load(atomic::Ordering::Acquire),
20622 0,
20623 "Should not restart LSP server on an unrelated change"
20624 );
20625
20626 update_test_project_settings(cx, &|project_settings| {
20627 project_settings.lsp.0.insert(
20628 "Some other server name".into(),
20629 LspSettings {
20630 binary: None,
20631 settings: None,
20632 initialization_options: Some(json!({
20633 "some other init value": false
20634 })),
20635 enable_lsp_tasks: false,
20636 fetch: None,
20637 },
20638 );
20639 });
20640 cx.executor().run_until_parked();
20641 assert_eq!(
20642 server_restarts.load(atomic::Ordering::Acquire),
20643 0,
20644 "Should not restart LSP server on an unrelated LSP settings change"
20645 );
20646
20647 update_test_project_settings(cx, &|project_settings| {
20648 project_settings.lsp.0.insert(
20649 language_server_name.into(),
20650 LspSettings {
20651 binary: None,
20652 settings: None,
20653 initialization_options: Some(json!({
20654 "anotherInitValue": false
20655 })),
20656 enable_lsp_tasks: false,
20657 fetch: None,
20658 },
20659 );
20660 });
20661 cx.executor().run_until_parked();
20662 assert_eq!(
20663 server_restarts.load(atomic::Ordering::Acquire),
20664 1,
20665 "Should restart LSP server on a related LSP settings change"
20666 );
20667
20668 update_test_project_settings(cx, &|project_settings| {
20669 project_settings.lsp.0.insert(
20670 language_server_name.into(),
20671 LspSettings {
20672 binary: None,
20673 settings: None,
20674 initialization_options: Some(json!({
20675 "anotherInitValue": false
20676 })),
20677 enable_lsp_tasks: false,
20678 fetch: None,
20679 },
20680 );
20681 });
20682 cx.executor().run_until_parked();
20683 assert_eq!(
20684 server_restarts.load(atomic::Ordering::Acquire),
20685 1,
20686 "Should not restart LSP server on a related LSP settings change that is the same"
20687 );
20688
20689 update_test_project_settings(cx, &|project_settings| {
20690 project_settings.lsp.0.insert(
20691 language_server_name.into(),
20692 LspSettings {
20693 binary: None,
20694 settings: None,
20695 initialization_options: None,
20696 enable_lsp_tasks: false,
20697 fetch: None,
20698 },
20699 );
20700 });
20701 cx.executor().run_until_parked();
20702 assert_eq!(
20703 server_restarts.load(atomic::Ordering::Acquire),
20704 2,
20705 "Should restart LSP server on another related LSP settings change"
20706 );
20707}
20708
20709#[gpui::test]
20710async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
20711 init_test(cx, |_| {});
20712
20713 let mut cx = EditorLspTestContext::new_rust(
20714 lsp::ServerCapabilities {
20715 completion_provider: Some(lsp::CompletionOptions {
20716 trigger_characters: Some(vec![".".to_string()]),
20717 resolve_provider: Some(true),
20718 ..Default::default()
20719 }),
20720 ..Default::default()
20721 },
20722 cx,
20723 )
20724 .await;
20725
20726 cx.set_state("fn main() { let a = 2ˇ; }");
20727 cx.simulate_keystroke(".");
20728 let completion_item = lsp::CompletionItem {
20729 label: "some".into(),
20730 kind: Some(lsp::CompletionItemKind::SNIPPET),
20731 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
20732 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
20733 kind: lsp::MarkupKind::Markdown,
20734 value: "```rust\nSome(2)\n```".to_string(),
20735 })),
20736 deprecated: Some(false),
20737 sort_text: Some("fffffff2".to_string()),
20738 filter_text: Some("some".to_string()),
20739 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
20740 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20741 range: lsp::Range {
20742 start: lsp::Position {
20743 line: 0,
20744 character: 22,
20745 },
20746 end: lsp::Position {
20747 line: 0,
20748 character: 22,
20749 },
20750 },
20751 new_text: "Some(2)".to_string(),
20752 })),
20753 additional_text_edits: Some(vec![lsp::TextEdit {
20754 range: lsp::Range {
20755 start: lsp::Position {
20756 line: 0,
20757 character: 20,
20758 },
20759 end: lsp::Position {
20760 line: 0,
20761 character: 22,
20762 },
20763 },
20764 new_text: "".to_string(),
20765 }]),
20766 ..Default::default()
20767 };
20768
20769 let closure_completion_item = completion_item.clone();
20770 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20771 let task_completion_item = closure_completion_item.clone();
20772 async move {
20773 Ok(Some(lsp::CompletionResponse::Array(vec![
20774 task_completion_item,
20775 ])))
20776 }
20777 });
20778
20779 request.next().await;
20780
20781 cx.condition(|editor, _| editor.context_menu_visible())
20782 .await;
20783 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20784 editor
20785 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20786 .unwrap()
20787 });
20788 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
20789
20790 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20791 let task_completion_item = completion_item.clone();
20792 async move { Ok(task_completion_item) }
20793 })
20794 .next()
20795 .await
20796 .unwrap();
20797 apply_additional_edits.await.unwrap();
20798 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
20799}
20800
20801#[gpui::test]
20802async fn test_completions_with_additional_edits_undo(cx: &mut TestAppContext) {
20803 init_test(cx, |_| {});
20804
20805 let mut cx = EditorLspTestContext::new_rust(
20806 lsp::ServerCapabilities {
20807 completion_provider: Some(lsp::CompletionOptions {
20808 trigger_characters: Some(vec![".".to_string()]),
20809 resolve_provider: Some(true),
20810 ..Default::default()
20811 }),
20812 ..Default::default()
20813 },
20814 cx,
20815 )
20816 .await;
20817
20818 cx.set_state("fn main() { let a = 2ˇ; }");
20819 cx.simulate_keystroke(".");
20820 let completion_item = lsp::CompletionItem {
20821 label: "some".into(),
20822 kind: Some(lsp::CompletionItemKind::SNIPPET),
20823 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
20824 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
20825 kind: lsp::MarkupKind::Markdown,
20826 value: "```rust\nSome(2)\n```".to_string(),
20827 })),
20828 deprecated: Some(false),
20829 sort_text: Some("fffffff2".to_string()),
20830 filter_text: Some("some".to_string()),
20831 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
20832 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20833 range: lsp::Range {
20834 start: lsp::Position {
20835 line: 0,
20836 character: 22,
20837 },
20838 end: lsp::Position {
20839 line: 0,
20840 character: 22,
20841 },
20842 },
20843 new_text: "Some(2)".to_string(),
20844 })),
20845 additional_text_edits: Some(vec![lsp::TextEdit {
20846 range: lsp::Range {
20847 start: lsp::Position {
20848 line: 0,
20849 character: 20,
20850 },
20851 end: lsp::Position {
20852 line: 0,
20853 character: 22,
20854 },
20855 },
20856 new_text: "".to_string(),
20857 }]),
20858 ..Default::default()
20859 };
20860
20861 let closure_completion_item = completion_item.clone();
20862 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20863 let task_completion_item = closure_completion_item.clone();
20864 async move {
20865 Ok(Some(lsp::CompletionResponse::Array(vec![
20866 task_completion_item,
20867 ])))
20868 }
20869 });
20870
20871 request.next().await;
20872
20873 cx.condition(|editor, _| editor.context_menu_visible())
20874 .await;
20875 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20876 editor
20877 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20878 .unwrap()
20879 });
20880 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
20881
20882 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20883 let task_completion_item = completion_item.clone();
20884 async move { Ok(task_completion_item) }
20885 })
20886 .next()
20887 .await
20888 .unwrap();
20889 apply_additional_edits.await.unwrap();
20890 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
20891
20892 cx.update_editor(|editor, window, cx| {
20893 editor.undo(&crate::Undo, window, cx);
20894 });
20895 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
20896}
20897
20898#[gpui::test]
20899async fn test_completions_with_additional_edits_and_multiple_cursors(cx: &mut TestAppContext) {
20900 init_test(cx, |_| {});
20901
20902 let mut cx = EditorLspTestContext::new_typescript(
20903 lsp::ServerCapabilities {
20904 completion_provider: Some(lsp::CompletionOptions {
20905 resolve_provider: Some(true),
20906 ..Default::default()
20907 }),
20908 ..Default::default()
20909 },
20910 cx,
20911 )
20912 .await;
20913
20914 cx.set_state(
20915 "import { «Fooˇ» } from './types';\n\nclass Bar {\n method(): «Fooˇ» { return new Foo(); }\n}",
20916 );
20917
20918 cx.simulate_keystroke("F");
20919 cx.simulate_keystroke("o");
20920
20921 let completion_item = lsp::CompletionItem {
20922 label: "FooBar".into(),
20923 kind: Some(lsp::CompletionItemKind::CLASS),
20924 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20925 range: lsp::Range {
20926 start: lsp::Position {
20927 line: 3,
20928 character: 14,
20929 },
20930 end: lsp::Position {
20931 line: 3,
20932 character: 16,
20933 },
20934 },
20935 new_text: "FooBar".to_string(),
20936 })),
20937 additional_text_edits: Some(vec![lsp::TextEdit {
20938 range: lsp::Range {
20939 start: lsp::Position {
20940 line: 0,
20941 character: 9,
20942 },
20943 end: lsp::Position {
20944 line: 0,
20945 character: 11,
20946 },
20947 },
20948 new_text: "FooBar".to_string(),
20949 }]),
20950 ..Default::default()
20951 };
20952
20953 let closure_completion_item = completion_item.clone();
20954 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20955 let task_completion_item = closure_completion_item.clone();
20956 async move {
20957 Ok(Some(lsp::CompletionResponse::Array(vec![
20958 task_completion_item,
20959 ])))
20960 }
20961 });
20962
20963 request.next().await;
20964
20965 cx.condition(|editor, _| editor.context_menu_visible())
20966 .await;
20967 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20968 editor
20969 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20970 .unwrap()
20971 });
20972
20973 cx.assert_editor_state(
20974 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
20975 );
20976
20977 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20978 let task_completion_item = completion_item.clone();
20979 async move { Ok(task_completion_item) }
20980 })
20981 .next()
20982 .await
20983 .unwrap();
20984
20985 apply_additional_edits.await.unwrap();
20986
20987 cx.assert_editor_state(
20988 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
20989 );
20990}
20991
20992#[gpui::test]
20993async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
20994 init_test(cx, |_| {});
20995
20996 let mut cx = EditorLspTestContext::new_rust(
20997 lsp::ServerCapabilities {
20998 completion_provider: Some(lsp::CompletionOptions {
20999 trigger_characters: Some(vec![".".to_string()]),
21000 resolve_provider: Some(true),
21001 ..Default::default()
21002 }),
21003 ..Default::default()
21004 },
21005 cx,
21006 )
21007 .await;
21008
21009 cx.set_state("fn main() { let a = 2ˇ; }");
21010 cx.simulate_keystroke(".");
21011
21012 let item1 = lsp::CompletionItem {
21013 label: "method id()".to_string(),
21014 filter_text: Some("id".to_string()),
21015 detail: None,
21016 documentation: None,
21017 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21018 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21019 new_text: ".id".to_string(),
21020 })),
21021 ..lsp::CompletionItem::default()
21022 };
21023
21024 let item2 = lsp::CompletionItem {
21025 label: "other".to_string(),
21026 filter_text: Some("other".to_string()),
21027 detail: None,
21028 documentation: None,
21029 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21030 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21031 new_text: ".other".to_string(),
21032 })),
21033 ..lsp::CompletionItem::default()
21034 };
21035
21036 let item1 = item1.clone();
21037 cx.set_request_handler::<lsp::request::Completion, _, _>({
21038 let item1 = item1.clone();
21039 move |_, _, _| {
21040 let item1 = item1.clone();
21041 let item2 = item2.clone();
21042 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
21043 }
21044 })
21045 .next()
21046 .await;
21047
21048 cx.condition(|editor, _| editor.context_menu_visible())
21049 .await;
21050 cx.update_editor(|editor, _, _| {
21051 let context_menu = editor.context_menu.borrow_mut();
21052 let context_menu = context_menu
21053 .as_ref()
21054 .expect("Should have the context menu deployed");
21055 match context_menu {
21056 CodeContextMenu::Completions(completions_menu) => {
21057 let completions = completions_menu.completions.borrow_mut();
21058 assert_eq!(
21059 completions
21060 .iter()
21061 .map(|completion| &completion.label.text)
21062 .collect::<Vec<_>>(),
21063 vec!["method id()", "other"]
21064 )
21065 }
21066 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21067 }
21068 });
21069
21070 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
21071 let item1 = item1.clone();
21072 move |_, item_to_resolve, _| {
21073 let item1 = item1.clone();
21074 async move {
21075 if item1 == item_to_resolve {
21076 Ok(lsp::CompletionItem {
21077 label: "method id()".to_string(),
21078 filter_text: Some("id".to_string()),
21079 detail: Some("Now resolved!".to_string()),
21080 documentation: Some(lsp::Documentation::String("Docs".to_string())),
21081 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21082 range: lsp::Range::new(
21083 lsp::Position::new(0, 22),
21084 lsp::Position::new(0, 22),
21085 ),
21086 new_text: ".id".to_string(),
21087 })),
21088 ..lsp::CompletionItem::default()
21089 })
21090 } else {
21091 Ok(item_to_resolve)
21092 }
21093 }
21094 }
21095 })
21096 .next()
21097 .await
21098 .unwrap();
21099 cx.run_until_parked();
21100
21101 cx.update_editor(|editor, window, cx| {
21102 editor.context_menu_next(&Default::default(), window, cx);
21103 });
21104 cx.run_until_parked();
21105
21106 cx.update_editor(|editor, _, _| {
21107 let context_menu = editor.context_menu.borrow_mut();
21108 let context_menu = context_menu
21109 .as_ref()
21110 .expect("Should have the context menu deployed");
21111 match context_menu {
21112 CodeContextMenu::Completions(completions_menu) => {
21113 let completions = completions_menu.completions.borrow_mut();
21114 assert_eq!(
21115 completions
21116 .iter()
21117 .map(|completion| &completion.label.text)
21118 .collect::<Vec<_>>(),
21119 vec!["method id() Now resolved!", "other"],
21120 "Should update first completion label, but not second as the filter text did not match."
21121 );
21122 }
21123 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21124 }
21125 });
21126}
21127
21128#[gpui::test]
21129async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
21130 init_test(cx, |_| {});
21131 let mut cx = EditorLspTestContext::new_rust(
21132 lsp::ServerCapabilities {
21133 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
21134 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
21135 completion_provider: Some(lsp::CompletionOptions {
21136 resolve_provider: Some(true),
21137 ..Default::default()
21138 }),
21139 ..Default::default()
21140 },
21141 cx,
21142 )
21143 .await;
21144 cx.set_state(indoc! {"
21145 struct TestStruct {
21146 field: i32
21147 }
21148
21149 fn mainˇ() {
21150 let unused_var = 42;
21151 let test_struct = TestStruct { field: 42 };
21152 }
21153 "});
21154 let symbol_range = cx.lsp_range(indoc! {"
21155 struct TestStruct {
21156 field: i32
21157 }
21158
21159 «fn main»() {
21160 let unused_var = 42;
21161 let test_struct = TestStruct { field: 42 };
21162 }
21163 "});
21164 let mut hover_requests =
21165 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
21166 Ok(Some(lsp::Hover {
21167 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
21168 kind: lsp::MarkupKind::Markdown,
21169 value: "Function documentation".to_string(),
21170 }),
21171 range: Some(symbol_range),
21172 }))
21173 });
21174
21175 // Case 1: Test that code action menu hide hover popover
21176 cx.dispatch_action(Hover);
21177 hover_requests.next().await;
21178 cx.condition(|editor, _| editor.hover_state.visible()).await;
21179 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
21180 move |_, _, _| async move {
21181 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
21182 lsp::CodeAction {
21183 title: "Remove unused variable".to_string(),
21184 kind: Some(CodeActionKind::QUICKFIX),
21185 edit: Some(lsp::WorkspaceEdit {
21186 changes: Some(
21187 [(
21188 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
21189 vec![lsp::TextEdit {
21190 range: lsp::Range::new(
21191 lsp::Position::new(5, 4),
21192 lsp::Position::new(5, 27),
21193 ),
21194 new_text: "".to_string(),
21195 }],
21196 )]
21197 .into_iter()
21198 .collect(),
21199 ),
21200 ..Default::default()
21201 }),
21202 ..Default::default()
21203 },
21204 )]))
21205 },
21206 );
21207 cx.update_editor(|editor, window, cx| {
21208 editor.toggle_code_actions(
21209 &ToggleCodeActions {
21210 deployed_from: None,
21211 quick_launch: false,
21212 },
21213 window,
21214 cx,
21215 );
21216 });
21217 code_action_requests.next().await;
21218 cx.run_until_parked();
21219 cx.condition(|editor, _| editor.context_menu_visible())
21220 .await;
21221 cx.update_editor(|editor, _, _| {
21222 assert!(
21223 !editor.hover_state.visible(),
21224 "Hover popover should be hidden when code action menu is shown"
21225 );
21226 // Hide code actions
21227 editor.context_menu.take();
21228 });
21229
21230 // Case 2: Test that code completions hide hover popover
21231 cx.dispatch_action(Hover);
21232 hover_requests.next().await;
21233 cx.condition(|editor, _| editor.hover_state.visible()).await;
21234 let counter = Arc::new(AtomicUsize::new(0));
21235 let mut completion_requests =
21236 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21237 let counter = counter.clone();
21238 async move {
21239 counter.fetch_add(1, atomic::Ordering::Release);
21240 Ok(Some(lsp::CompletionResponse::Array(vec![
21241 lsp::CompletionItem {
21242 label: "main".into(),
21243 kind: Some(lsp::CompletionItemKind::FUNCTION),
21244 detail: Some("() -> ()".to_string()),
21245 ..Default::default()
21246 },
21247 lsp::CompletionItem {
21248 label: "TestStruct".into(),
21249 kind: Some(lsp::CompletionItemKind::STRUCT),
21250 detail: Some("struct TestStruct".to_string()),
21251 ..Default::default()
21252 },
21253 ])))
21254 }
21255 });
21256 cx.update_editor(|editor, window, cx| {
21257 editor.show_completions(&ShowCompletions, window, cx);
21258 });
21259 completion_requests.next().await;
21260 cx.condition(|editor, _| editor.context_menu_visible())
21261 .await;
21262 cx.update_editor(|editor, _, _| {
21263 assert!(
21264 !editor.hover_state.visible(),
21265 "Hover popover should be hidden when completion menu is shown"
21266 );
21267 });
21268}
21269
21270#[gpui::test]
21271async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
21272 init_test(cx, |_| {});
21273
21274 let mut cx = EditorLspTestContext::new_rust(
21275 lsp::ServerCapabilities {
21276 completion_provider: Some(lsp::CompletionOptions {
21277 trigger_characters: Some(vec![".".to_string()]),
21278 resolve_provider: Some(true),
21279 ..Default::default()
21280 }),
21281 ..Default::default()
21282 },
21283 cx,
21284 )
21285 .await;
21286
21287 cx.set_state("fn main() { let a = 2ˇ; }");
21288 cx.simulate_keystroke(".");
21289
21290 let unresolved_item_1 = lsp::CompletionItem {
21291 label: "id".to_string(),
21292 filter_text: Some("id".to_string()),
21293 detail: None,
21294 documentation: None,
21295 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21296 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21297 new_text: ".id".to_string(),
21298 })),
21299 ..lsp::CompletionItem::default()
21300 };
21301 let resolved_item_1 = lsp::CompletionItem {
21302 additional_text_edits: Some(vec![lsp::TextEdit {
21303 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
21304 new_text: "!!".to_string(),
21305 }]),
21306 ..unresolved_item_1.clone()
21307 };
21308 let unresolved_item_2 = lsp::CompletionItem {
21309 label: "other".to_string(),
21310 filter_text: Some("other".to_string()),
21311 detail: None,
21312 documentation: None,
21313 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21314 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
21315 new_text: ".other".to_string(),
21316 })),
21317 ..lsp::CompletionItem::default()
21318 };
21319 let resolved_item_2 = lsp::CompletionItem {
21320 additional_text_edits: Some(vec![lsp::TextEdit {
21321 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
21322 new_text: "??".to_string(),
21323 }]),
21324 ..unresolved_item_2.clone()
21325 };
21326
21327 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
21328 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
21329 cx.lsp
21330 .server
21331 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
21332 let unresolved_item_1 = unresolved_item_1.clone();
21333 let resolved_item_1 = resolved_item_1.clone();
21334 let unresolved_item_2 = unresolved_item_2.clone();
21335 let resolved_item_2 = resolved_item_2.clone();
21336 let resolve_requests_1 = resolve_requests_1.clone();
21337 let resolve_requests_2 = resolve_requests_2.clone();
21338 move |unresolved_request, _| {
21339 let unresolved_item_1 = unresolved_item_1.clone();
21340 let resolved_item_1 = resolved_item_1.clone();
21341 let unresolved_item_2 = unresolved_item_2.clone();
21342 let resolved_item_2 = resolved_item_2.clone();
21343 let resolve_requests_1 = resolve_requests_1.clone();
21344 let resolve_requests_2 = resolve_requests_2.clone();
21345 async move {
21346 if unresolved_request == unresolved_item_1 {
21347 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
21348 Ok(resolved_item_1.clone())
21349 } else if unresolved_request == unresolved_item_2 {
21350 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
21351 Ok(resolved_item_2.clone())
21352 } else {
21353 panic!("Unexpected completion item {unresolved_request:?}")
21354 }
21355 }
21356 }
21357 })
21358 .detach();
21359
21360 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21361 let unresolved_item_1 = unresolved_item_1.clone();
21362 let unresolved_item_2 = unresolved_item_2.clone();
21363 async move {
21364 Ok(Some(lsp::CompletionResponse::Array(vec![
21365 unresolved_item_1,
21366 unresolved_item_2,
21367 ])))
21368 }
21369 })
21370 .next()
21371 .await;
21372
21373 cx.condition(|editor, _| editor.context_menu_visible())
21374 .await;
21375 cx.update_editor(|editor, _, _| {
21376 let context_menu = editor.context_menu.borrow_mut();
21377 let context_menu = context_menu
21378 .as_ref()
21379 .expect("Should have the context menu deployed");
21380 match context_menu {
21381 CodeContextMenu::Completions(completions_menu) => {
21382 let completions = completions_menu.completions.borrow_mut();
21383 assert_eq!(
21384 completions
21385 .iter()
21386 .map(|completion| &completion.label.text)
21387 .collect::<Vec<_>>(),
21388 vec!["id", "other"]
21389 )
21390 }
21391 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
21392 }
21393 });
21394 cx.run_until_parked();
21395
21396 cx.update_editor(|editor, window, cx| {
21397 editor.context_menu_next(&ContextMenuNext, window, cx);
21398 });
21399 cx.run_until_parked();
21400 cx.update_editor(|editor, window, cx| {
21401 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
21402 });
21403 cx.run_until_parked();
21404 cx.update_editor(|editor, window, cx| {
21405 editor.context_menu_next(&ContextMenuNext, window, cx);
21406 });
21407 cx.run_until_parked();
21408 cx.update_editor(|editor, window, cx| {
21409 editor
21410 .compose_completion(&ComposeCompletion::default(), window, cx)
21411 .expect("No task returned")
21412 })
21413 .await
21414 .expect("Completion failed");
21415 cx.run_until_parked();
21416
21417 cx.update_editor(|editor, _, cx| {
21418 assert_eq!(
21419 resolve_requests_1.load(atomic::Ordering::Acquire),
21420 1,
21421 "Should always resolve once despite multiple selections"
21422 );
21423 assert_eq!(
21424 resolve_requests_2.load(atomic::Ordering::Acquire),
21425 1,
21426 "Should always resolve once after multiple selections and applying the completion"
21427 );
21428 assert_eq!(
21429 editor.text(cx),
21430 "fn main() { let a = ??.other; }",
21431 "Should use resolved data when applying the completion"
21432 );
21433 });
21434}
21435
21436#[gpui::test]
21437async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
21438 init_test(cx, |_| {});
21439
21440 let item_0 = lsp::CompletionItem {
21441 label: "abs".into(),
21442 insert_text: Some("abs".into()),
21443 data: Some(json!({ "very": "special"})),
21444 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
21445 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21446 lsp::InsertReplaceEdit {
21447 new_text: "abs".to_string(),
21448 insert: lsp::Range::default(),
21449 replace: lsp::Range::default(),
21450 },
21451 )),
21452 ..lsp::CompletionItem::default()
21453 };
21454 let items = iter::once(item_0.clone())
21455 .chain((11..51).map(|i| lsp::CompletionItem {
21456 label: format!("item_{}", i),
21457 insert_text: Some(format!("item_{}", i)),
21458 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
21459 ..lsp::CompletionItem::default()
21460 }))
21461 .collect::<Vec<_>>();
21462
21463 let default_commit_characters = vec!["?".to_string()];
21464 let default_data = json!({ "default": "data"});
21465 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
21466 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
21467 let default_edit_range = lsp::Range {
21468 start: lsp::Position {
21469 line: 0,
21470 character: 5,
21471 },
21472 end: lsp::Position {
21473 line: 0,
21474 character: 5,
21475 },
21476 };
21477
21478 let mut cx = EditorLspTestContext::new_rust(
21479 lsp::ServerCapabilities {
21480 completion_provider: Some(lsp::CompletionOptions {
21481 trigger_characters: Some(vec![".".to_string()]),
21482 resolve_provider: Some(true),
21483 ..Default::default()
21484 }),
21485 ..Default::default()
21486 },
21487 cx,
21488 )
21489 .await;
21490
21491 cx.set_state("fn main() { let a = 2ˇ; }");
21492 cx.simulate_keystroke(".");
21493
21494 let completion_data = default_data.clone();
21495 let completion_characters = default_commit_characters.clone();
21496 let completion_items = items.clone();
21497 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
21498 let default_data = completion_data.clone();
21499 let default_commit_characters = completion_characters.clone();
21500 let items = completion_items.clone();
21501 async move {
21502 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21503 items,
21504 item_defaults: Some(lsp::CompletionListItemDefaults {
21505 data: Some(default_data.clone()),
21506 commit_characters: Some(default_commit_characters.clone()),
21507 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
21508 default_edit_range,
21509 )),
21510 insert_text_format: Some(default_insert_text_format),
21511 insert_text_mode: Some(default_insert_text_mode),
21512 }),
21513 ..lsp::CompletionList::default()
21514 })))
21515 }
21516 })
21517 .next()
21518 .await;
21519
21520 let resolved_items = Arc::new(Mutex::new(Vec::new()));
21521 cx.lsp
21522 .server
21523 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
21524 let closure_resolved_items = resolved_items.clone();
21525 move |item_to_resolve, _| {
21526 let closure_resolved_items = closure_resolved_items.clone();
21527 async move {
21528 closure_resolved_items.lock().push(item_to_resolve.clone());
21529 Ok(item_to_resolve)
21530 }
21531 }
21532 })
21533 .detach();
21534
21535 cx.condition(|editor, _| editor.context_menu_visible())
21536 .await;
21537 cx.run_until_parked();
21538 cx.update_editor(|editor, _, _| {
21539 let menu = editor.context_menu.borrow_mut();
21540 match menu.as_ref().expect("should have the completions menu") {
21541 CodeContextMenu::Completions(completions_menu) => {
21542 assert_eq!(
21543 completions_menu
21544 .entries
21545 .borrow()
21546 .iter()
21547 .map(|mat| mat.string.clone())
21548 .collect::<Vec<String>>(),
21549 items
21550 .iter()
21551 .map(|completion| completion.label.clone())
21552 .collect::<Vec<String>>()
21553 );
21554 }
21555 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
21556 }
21557 });
21558 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
21559 // with 4 from the end.
21560 assert_eq!(
21561 *resolved_items.lock(),
21562 [&items[0..16], &items[items.len() - 4..items.len()]]
21563 .concat()
21564 .iter()
21565 .cloned()
21566 .map(|mut item| {
21567 if item.data.is_none() {
21568 item.data = Some(default_data.clone());
21569 }
21570 item
21571 })
21572 .collect::<Vec<lsp::CompletionItem>>(),
21573 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
21574 );
21575 resolved_items.lock().clear();
21576
21577 cx.update_editor(|editor, window, cx| {
21578 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
21579 });
21580 cx.run_until_parked();
21581 // Completions that have already been resolved are skipped.
21582 assert_eq!(
21583 *resolved_items.lock(),
21584 items[items.len() - 17..items.len() - 4]
21585 .iter()
21586 .cloned()
21587 .map(|mut item| {
21588 if item.data.is_none() {
21589 item.data = Some(default_data.clone());
21590 }
21591 item
21592 })
21593 .collect::<Vec<lsp::CompletionItem>>()
21594 );
21595 resolved_items.lock().clear();
21596}
21597
21598#[gpui::test]
21599async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
21600 init_test(cx, |_| {});
21601
21602 let mut cx = EditorLspTestContext::new(
21603 Language::new(
21604 LanguageConfig {
21605 matcher: LanguageMatcher {
21606 path_suffixes: vec!["jsx".into()],
21607 ..Default::default()
21608 },
21609 overrides: [(
21610 "element".into(),
21611 LanguageConfigOverride {
21612 completion_query_characters: Override::Set(['-'].into_iter().collect()),
21613 ..Default::default()
21614 },
21615 )]
21616 .into_iter()
21617 .collect(),
21618 ..Default::default()
21619 },
21620 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
21621 )
21622 .with_override_query("(jsx_self_closing_element) @element")
21623 .unwrap(),
21624 lsp::ServerCapabilities {
21625 completion_provider: Some(lsp::CompletionOptions {
21626 trigger_characters: Some(vec![":".to_string()]),
21627 ..Default::default()
21628 }),
21629 ..Default::default()
21630 },
21631 cx,
21632 )
21633 .await;
21634
21635 cx.lsp
21636 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21637 Ok(Some(lsp::CompletionResponse::Array(vec![
21638 lsp::CompletionItem {
21639 label: "bg-blue".into(),
21640 ..Default::default()
21641 },
21642 lsp::CompletionItem {
21643 label: "bg-red".into(),
21644 ..Default::default()
21645 },
21646 lsp::CompletionItem {
21647 label: "bg-yellow".into(),
21648 ..Default::default()
21649 },
21650 ])))
21651 });
21652
21653 cx.set_state(r#"<p class="bgˇ" />"#);
21654
21655 // Trigger completion when typing a dash, because the dash is an extra
21656 // word character in the 'element' scope, which contains the cursor.
21657 cx.simulate_keystroke("-");
21658 cx.executor().run_until_parked();
21659 cx.update_editor(|editor, _, _| {
21660 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
21661 {
21662 assert_eq!(
21663 completion_menu_entries(menu),
21664 &["bg-blue", "bg-red", "bg-yellow"]
21665 );
21666 } else {
21667 panic!("expected completion menu to be open");
21668 }
21669 });
21670
21671 cx.simulate_keystroke("l");
21672 cx.executor().run_until_parked();
21673 cx.update_editor(|editor, _, _| {
21674 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
21675 {
21676 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
21677 } else {
21678 panic!("expected completion menu to be open");
21679 }
21680 });
21681
21682 // When filtering completions, consider the character after the '-' to
21683 // be the start of a subword.
21684 cx.set_state(r#"<p class="yelˇ" />"#);
21685 cx.simulate_keystroke("l");
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!(completion_menu_entries(menu), &["bg-yellow"]);
21691 } else {
21692 panic!("expected completion menu to be open");
21693 }
21694 });
21695}
21696
21697fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
21698 let entries = menu.entries.borrow();
21699 entries.iter().map(|mat| mat.string.clone()).collect()
21700}
21701
21702#[gpui::test]
21703async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
21704 init_test(cx, |settings| {
21705 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21706 });
21707
21708 let fs = FakeFs::new(cx.executor());
21709 fs.insert_file(path!("/file.ts"), Default::default()).await;
21710
21711 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
21712 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21713
21714 language_registry.add(Arc::new(Language::new(
21715 LanguageConfig {
21716 name: "TypeScript".into(),
21717 matcher: LanguageMatcher {
21718 path_suffixes: vec!["ts".to_string()],
21719 ..Default::default()
21720 },
21721 ..Default::default()
21722 },
21723 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21724 )));
21725 update_test_language_settings(cx, &|settings| {
21726 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21727 });
21728
21729 let test_plugin = "test_plugin";
21730 let _ = language_registry.register_fake_lsp(
21731 "TypeScript",
21732 FakeLspAdapter {
21733 prettier_plugins: vec![test_plugin],
21734 ..Default::default()
21735 },
21736 );
21737
21738 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21739 let buffer = project
21740 .update(cx, |project, cx| {
21741 project.open_local_buffer(path!("/file.ts"), cx)
21742 })
21743 .await
21744 .unwrap();
21745
21746 let buffer_text = "one\ntwo\nthree\n";
21747 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21748 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21749 editor.update_in(cx, |editor, window, cx| {
21750 editor.set_text(buffer_text, window, cx)
21751 });
21752
21753 editor
21754 .update_in(cx, |editor, window, cx| {
21755 editor.perform_format(
21756 project.clone(),
21757 FormatTrigger::Manual,
21758 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21759 window,
21760 cx,
21761 )
21762 })
21763 .unwrap()
21764 .await;
21765 assert_eq!(
21766 editor.update(cx, |editor, cx| editor.text(cx)),
21767 buffer_text.to_string() + prettier_format_suffix,
21768 "Test prettier formatting was not applied to the original buffer text",
21769 );
21770
21771 update_test_language_settings(cx, &|settings| {
21772 settings.defaults.formatter = Some(FormatterList::default())
21773 });
21774 let format = editor.update_in(cx, |editor, window, cx| {
21775 editor.perform_format(
21776 project.clone(),
21777 FormatTrigger::Manual,
21778 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21779 window,
21780 cx,
21781 )
21782 });
21783 format.await.unwrap();
21784 assert_eq!(
21785 editor.update(cx, |editor, cx| editor.text(cx)),
21786 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
21787 "Autoformatting (via test prettier) was not applied to the original buffer text",
21788 );
21789}
21790
21791#[gpui::test]
21792async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
21793 init_test(cx, |settings| {
21794 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21795 });
21796
21797 let fs = FakeFs::new(cx.executor());
21798 fs.insert_file(path!("/file.settings"), Default::default())
21799 .await;
21800
21801 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
21802 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21803
21804 let ts_lang = Arc::new(Language::new(
21805 LanguageConfig {
21806 name: "TypeScript".into(),
21807 matcher: LanguageMatcher {
21808 path_suffixes: vec!["ts".to_string()],
21809 ..LanguageMatcher::default()
21810 },
21811 prettier_parser_name: Some("typescript".to_string()),
21812 ..LanguageConfig::default()
21813 },
21814 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21815 ));
21816
21817 language_registry.add(ts_lang.clone());
21818
21819 update_test_language_settings(cx, &|settings| {
21820 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21821 });
21822
21823 let test_plugin = "test_plugin";
21824 let _ = language_registry.register_fake_lsp(
21825 "TypeScript",
21826 FakeLspAdapter {
21827 prettier_plugins: vec![test_plugin],
21828 ..Default::default()
21829 },
21830 );
21831
21832 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21833 let buffer = project
21834 .update(cx, |project, cx| {
21835 project.open_local_buffer(path!("/file.settings"), cx)
21836 })
21837 .await
21838 .unwrap();
21839
21840 project.update(cx, |project, cx| {
21841 project.set_language_for_buffer(&buffer, ts_lang, cx)
21842 });
21843
21844 let buffer_text = "one\ntwo\nthree\n";
21845 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21846 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21847 editor.update_in(cx, |editor, window, cx| {
21848 editor.set_text(buffer_text, window, cx)
21849 });
21850
21851 editor
21852 .update_in(cx, |editor, window, cx| {
21853 editor.perform_format(
21854 project.clone(),
21855 FormatTrigger::Manual,
21856 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21857 window,
21858 cx,
21859 )
21860 })
21861 .unwrap()
21862 .await;
21863 assert_eq!(
21864 editor.update(cx, |editor, cx| editor.text(cx)),
21865 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
21866 "Test prettier formatting was not applied to the original buffer text",
21867 );
21868
21869 update_test_language_settings(cx, &|settings| {
21870 settings.defaults.formatter = Some(FormatterList::default())
21871 });
21872 let format = editor.update_in(cx, |editor, window, cx| {
21873 editor.perform_format(
21874 project.clone(),
21875 FormatTrigger::Manual,
21876 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21877 window,
21878 cx,
21879 )
21880 });
21881 format.await.unwrap();
21882
21883 assert_eq!(
21884 editor.update(cx, |editor, cx| editor.text(cx)),
21885 buffer_text.to_string()
21886 + prettier_format_suffix
21887 + "\ntypescript\n"
21888 + prettier_format_suffix
21889 + "\ntypescript",
21890 "Autoformatting (via test prettier) was not applied to the original buffer text",
21891 );
21892}
21893
21894#[gpui::test]
21895async fn test_range_format_with_prettier(cx: &mut TestAppContext) {
21896 init_test(cx, |settings| {
21897 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21898 });
21899
21900 let fs = FakeFs::new(cx.executor());
21901 fs.insert_file(path!("/file.ts"), Default::default()).await;
21902
21903 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
21904 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21905
21906 language_registry.add(Arc::new(Language::new(
21907 LanguageConfig {
21908 name: "TypeScript".into(),
21909 matcher: LanguageMatcher {
21910 path_suffixes: vec!["ts".to_string()],
21911 ..Default::default()
21912 },
21913 ..Default::default()
21914 },
21915 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21916 )));
21917 update_test_language_settings(cx, &|settings| {
21918 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21919 });
21920
21921 let test_plugin = "test_plugin";
21922 let _ = language_registry.register_fake_lsp(
21923 "TypeScript",
21924 FakeLspAdapter {
21925 prettier_plugins: vec![test_plugin],
21926 ..Default::default()
21927 },
21928 );
21929
21930 let prettier_range_format_suffix = project::TEST_PRETTIER_RANGE_FORMAT_SUFFIX;
21931 let buffer = project
21932 .update(cx, |project, cx| {
21933 project.open_local_buffer(path!("/file.ts"), cx)
21934 })
21935 .await
21936 .unwrap();
21937
21938 let buffer_text = "one\ntwo\nthree\nfour\nfive\n";
21939 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21940 let (editor, cx) = cx.add_window_view(|window, cx| {
21941 build_editor_with_project(project.clone(), buffer, window, cx)
21942 });
21943 editor.update_in(cx, |editor, window, cx| {
21944 editor.set_text(buffer_text, window, cx)
21945 });
21946
21947 cx.executor().run_until_parked();
21948
21949 editor.update_in(cx, |editor, window, cx| {
21950 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
21951 s.select_ranges([Point::new(1, 0)..Point::new(3, 0)])
21952 });
21953 });
21954
21955 let format = editor
21956 .update_in(cx, |editor, window, cx| {
21957 editor.format_selections(&FormatSelections, window, cx)
21958 })
21959 .unwrap();
21960 format.await.unwrap();
21961
21962 assert_eq!(
21963 editor.update(cx, |editor, cx| editor.text(cx)),
21964 format!("one\ntwo{prettier_range_format_suffix}\nthree\nfour\nfive\n"),
21965 "Range formatting (via test prettier) was not applied to the buffer text",
21966 );
21967}
21968
21969#[gpui::test]
21970async fn test_range_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
21971 init_test(cx, |settings| {
21972 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21973 });
21974
21975 let fs = FakeFs::new(cx.executor());
21976 fs.insert_file(path!("/file.settings"), Default::default())
21977 .await;
21978
21979 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
21980 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21981
21982 let ts_lang = Arc::new(Language::new(
21983 LanguageConfig {
21984 name: "TypeScript".into(),
21985 matcher: LanguageMatcher {
21986 path_suffixes: vec!["ts".to_string()],
21987 ..LanguageMatcher::default()
21988 },
21989 prettier_parser_name: Some("typescript".to_string()),
21990 ..LanguageConfig::default()
21991 },
21992 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21993 ));
21994
21995 language_registry.add(ts_lang.clone());
21996
21997 update_test_language_settings(cx, &|settings| {
21998 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21999 });
22000
22001 let test_plugin = "test_plugin";
22002 let _ = language_registry.register_fake_lsp(
22003 "TypeScript",
22004 FakeLspAdapter {
22005 prettier_plugins: vec![test_plugin],
22006 ..Default::default()
22007 },
22008 );
22009
22010 let prettier_range_format_suffix = project::TEST_PRETTIER_RANGE_FORMAT_SUFFIX;
22011 let buffer = project
22012 .update(cx, |project, cx| {
22013 project.open_local_buffer(path!("/file.settings"), cx)
22014 })
22015 .await
22016 .unwrap();
22017
22018 project.update(cx, |project, cx| {
22019 project.set_language_for_buffer(&buffer, ts_lang, cx)
22020 });
22021
22022 let buffer_text = "one\ntwo\nthree\nfour\nfive\n";
22023 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
22024 let (editor, cx) = cx.add_window_view(|window, cx| {
22025 build_editor_with_project(project.clone(), buffer, window, cx)
22026 });
22027 editor.update_in(cx, |editor, window, cx| {
22028 editor.set_text(buffer_text, window, cx)
22029 });
22030
22031 cx.executor().run_until_parked();
22032
22033 editor.update_in(cx, |editor, window, cx| {
22034 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
22035 s.select_ranges([Point::new(1, 0)..Point::new(3, 0)])
22036 });
22037 });
22038
22039 let format = editor
22040 .update_in(cx, |editor, window, cx| {
22041 editor.format_selections(&FormatSelections, window, cx)
22042 })
22043 .unwrap();
22044 format.await.unwrap();
22045
22046 assert_eq!(
22047 editor.update(cx, |editor, cx| editor.text(cx)),
22048 format!("one\ntwo{prettier_range_format_suffix}\ntypescript\nthree\nfour\nfive\n"),
22049 "Range formatting (via test prettier) was not applied with explicit language",
22050 );
22051}
22052
22053#[gpui::test]
22054async fn test_addition_reverts(cx: &mut TestAppContext) {
22055 init_test(cx, |_| {});
22056 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22057 let base_text = indoc! {r#"
22058 struct Row;
22059 struct Row1;
22060 struct Row2;
22061
22062 struct Row4;
22063 struct Row5;
22064 struct Row6;
22065
22066 struct Row8;
22067 struct Row9;
22068 struct Row10;"#};
22069
22070 // When addition hunks are not adjacent to carets, no hunk revert is performed
22071 assert_hunk_revert(
22072 indoc! {r#"struct Row;
22073 struct Row1;
22074 struct Row1.1;
22075 struct Row1.2;
22076 struct Row2;ˇ
22077
22078 struct Row4;
22079 struct Row5;
22080 struct Row6;
22081
22082 struct Row8;
22083 ˇstruct Row9;
22084 struct Row9.1;
22085 struct Row9.2;
22086 struct Row9.3;
22087 struct Row10;"#},
22088 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
22089 indoc! {r#"struct Row;
22090 struct Row1;
22091 struct Row1.1;
22092 struct Row1.2;
22093 struct Row2;ˇ
22094
22095 struct Row4;
22096 struct Row5;
22097 struct Row6;
22098
22099 struct Row8;
22100 ˇstruct Row9;
22101 struct Row9.1;
22102 struct Row9.2;
22103 struct Row9.3;
22104 struct Row10;"#},
22105 base_text,
22106 &mut cx,
22107 );
22108 // Same for selections
22109 assert_hunk_revert(
22110 indoc! {r#"struct Row;
22111 struct Row1;
22112 struct Row2;
22113 struct Row2.1;
22114 struct Row2.2;
22115 «ˇ
22116 struct Row4;
22117 struct» Row5;
22118 «struct Row6;
22119 ˇ»
22120 struct Row9.1;
22121 struct Row9.2;
22122 struct Row9.3;
22123 struct Row8;
22124 struct Row9;
22125 struct Row10;"#},
22126 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
22127 indoc! {r#"struct Row;
22128 struct Row1;
22129 struct Row2;
22130 struct Row2.1;
22131 struct Row2.2;
22132 «ˇ
22133 struct Row4;
22134 struct» Row5;
22135 «struct Row6;
22136 ˇ»
22137 struct Row9.1;
22138 struct Row9.2;
22139 struct Row9.3;
22140 struct Row8;
22141 struct Row9;
22142 struct Row10;"#},
22143 base_text,
22144 &mut cx,
22145 );
22146
22147 // When carets and selections intersect the addition hunks, those are reverted.
22148 // Adjacent carets got merged.
22149 assert_hunk_revert(
22150 indoc! {r#"struct Row;
22151 ˇ// something on the top
22152 struct Row1;
22153 struct Row2;
22154 struct Roˇw3.1;
22155 struct Row2.2;
22156 struct Row2.3;ˇ
22157
22158 struct Row4;
22159 struct ˇRow5.1;
22160 struct Row5.2;
22161 struct «Rowˇ»5.3;
22162 struct Row5;
22163 struct Row6;
22164 ˇ
22165 struct Row9.1;
22166 struct «Rowˇ»9.2;
22167 struct «ˇRow»9.3;
22168 struct Row8;
22169 struct Row9;
22170 «ˇ// something on bottom»
22171 struct Row10;"#},
22172 vec![
22173 DiffHunkStatusKind::Added,
22174 DiffHunkStatusKind::Added,
22175 DiffHunkStatusKind::Added,
22176 DiffHunkStatusKind::Added,
22177 DiffHunkStatusKind::Added,
22178 ],
22179 indoc! {r#"struct Row;
22180 ˇstruct Row1;
22181 struct Row2;
22182 ˇ
22183 struct Row4;
22184 ˇstruct Row5;
22185 struct Row6;
22186 ˇ
22187 ˇstruct Row8;
22188 struct Row9;
22189 ˇstruct Row10;"#},
22190 base_text,
22191 &mut cx,
22192 );
22193}
22194
22195#[gpui::test]
22196async fn test_modification_reverts(cx: &mut TestAppContext) {
22197 init_test(cx, |_| {});
22198 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22199 let base_text = indoc! {r#"
22200 struct Row;
22201 struct Row1;
22202 struct Row2;
22203
22204 struct Row4;
22205 struct Row5;
22206 struct Row6;
22207
22208 struct Row8;
22209 struct Row9;
22210 struct Row10;"#};
22211
22212 // Modification hunks behave the same as the addition ones.
22213 assert_hunk_revert(
22214 indoc! {r#"struct Row;
22215 struct Row1;
22216 struct Row33;
22217 ˇ
22218 struct Row4;
22219 struct Row5;
22220 struct Row6;
22221 ˇ
22222 struct Row99;
22223 struct Row9;
22224 struct Row10;"#},
22225 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
22226 indoc! {r#"struct Row;
22227 struct Row1;
22228 struct Row33;
22229 ˇ
22230 struct Row4;
22231 struct Row5;
22232 struct Row6;
22233 ˇ
22234 struct Row99;
22235 struct Row9;
22236 struct Row10;"#},
22237 base_text,
22238 &mut cx,
22239 );
22240 assert_hunk_revert(
22241 indoc! {r#"struct Row;
22242 struct Row1;
22243 struct Row33;
22244 «ˇ
22245 struct Row4;
22246 struct» Row5;
22247 «struct Row6;
22248 ˇ»
22249 struct Row99;
22250 struct Row9;
22251 struct Row10;"#},
22252 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
22253 indoc! {r#"struct Row;
22254 struct Row1;
22255 struct Row33;
22256 «ˇ
22257 struct Row4;
22258 struct» Row5;
22259 «struct Row6;
22260 ˇ»
22261 struct Row99;
22262 struct Row9;
22263 struct Row10;"#},
22264 base_text,
22265 &mut cx,
22266 );
22267
22268 assert_hunk_revert(
22269 indoc! {r#"ˇstruct Row1.1;
22270 struct Row1;
22271 «ˇstr»uct Row22;
22272
22273 struct ˇRow44;
22274 struct Row5;
22275 struct «Rˇ»ow66;ˇ
22276
22277 «struˇ»ct Row88;
22278 struct Row9;
22279 struct Row1011;ˇ"#},
22280 vec![
22281 DiffHunkStatusKind::Modified,
22282 DiffHunkStatusKind::Modified,
22283 DiffHunkStatusKind::Modified,
22284 DiffHunkStatusKind::Modified,
22285 DiffHunkStatusKind::Modified,
22286 DiffHunkStatusKind::Modified,
22287 ],
22288 indoc! {r#"struct Row;
22289 ˇstruct Row1;
22290 struct Row2;
22291 ˇ
22292 struct Row4;
22293 ˇstruct Row5;
22294 struct Row6;
22295 ˇ
22296 struct Row8;
22297 ˇstruct Row9;
22298 struct Row10;ˇ"#},
22299 base_text,
22300 &mut cx,
22301 );
22302}
22303
22304#[gpui::test]
22305async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
22306 init_test(cx, |_| {});
22307 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22308 let base_text = indoc! {r#"
22309 one
22310
22311 two
22312 three
22313 "#};
22314
22315 cx.set_head_text(base_text);
22316 cx.set_state("\nˇ\n");
22317 cx.executor().run_until_parked();
22318 cx.update_editor(|editor, _window, cx| {
22319 editor.expand_selected_diff_hunks(cx);
22320 });
22321 cx.executor().run_until_parked();
22322 cx.update_editor(|editor, window, cx| {
22323 editor.backspace(&Default::default(), window, cx);
22324 });
22325 cx.run_until_parked();
22326 cx.assert_state_with_diff(
22327 indoc! {r#"
22328
22329 - two
22330 - threeˇ
22331 +
22332 "#}
22333 .to_string(),
22334 );
22335}
22336
22337#[gpui::test]
22338async fn test_deletion_reverts(cx: &mut TestAppContext) {
22339 init_test(cx, |_| {});
22340 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
22341 let base_text = indoc! {r#"struct Row;
22342struct Row1;
22343struct Row2;
22344
22345struct Row4;
22346struct Row5;
22347struct Row6;
22348
22349struct Row8;
22350struct Row9;
22351struct Row10;"#};
22352
22353 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
22354 assert_hunk_revert(
22355 indoc! {r#"struct Row;
22356 struct Row2;
22357
22358 ˇstruct Row4;
22359 struct Row5;
22360 struct Row6;
22361 ˇ
22362 struct Row8;
22363 struct Row10;"#},
22364 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22365 indoc! {r#"struct Row;
22366 struct Row2;
22367
22368 ˇstruct Row4;
22369 struct Row5;
22370 struct Row6;
22371 ˇ
22372 struct Row8;
22373 struct Row10;"#},
22374 base_text,
22375 &mut cx,
22376 );
22377 assert_hunk_revert(
22378 indoc! {r#"struct Row;
22379 struct Row2;
22380
22381 «ˇstruct Row4;
22382 struct» Row5;
22383 «struct Row6;
22384 ˇ»
22385 struct Row8;
22386 struct Row10;"#},
22387 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22388 indoc! {r#"struct Row;
22389 struct Row2;
22390
22391 «ˇstruct Row4;
22392 struct» Row5;
22393 «struct Row6;
22394 ˇ»
22395 struct Row8;
22396 struct Row10;"#},
22397 base_text,
22398 &mut cx,
22399 );
22400
22401 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
22402 assert_hunk_revert(
22403 indoc! {r#"struct Row;
22404 ˇstruct Row2;
22405
22406 struct Row4;
22407 struct Row5;
22408 struct Row6;
22409
22410 struct Row8;ˇ
22411 struct Row10;"#},
22412 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
22413 indoc! {r#"struct Row;
22414 struct Row1;
22415 ˇstruct Row2;
22416
22417 struct Row4;
22418 struct Row5;
22419 struct Row6;
22420
22421 struct Row8;ˇ
22422 struct Row9;
22423 struct Row10;"#},
22424 base_text,
22425 &mut cx,
22426 );
22427 assert_hunk_revert(
22428 indoc! {r#"struct Row;
22429 struct Row2«ˇ;
22430 struct Row4;
22431 struct» Row5;
22432 «struct Row6;
22433
22434 struct Row8;ˇ»
22435 struct Row10;"#},
22436 vec![
22437 DiffHunkStatusKind::Deleted,
22438 DiffHunkStatusKind::Deleted,
22439 DiffHunkStatusKind::Deleted,
22440 ],
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}
22456
22457#[gpui::test]
22458async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
22459 init_test(cx, |_| {});
22460
22461 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
22462 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
22463 let base_text_3 =
22464 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
22465
22466 let text_1 = edit_first_char_of_every_line(base_text_1);
22467 let text_2 = edit_first_char_of_every_line(base_text_2);
22468 let text_3 = edit_first_char_of_every_line(base_text_3);
22469
22470 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
22471 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
22472 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
22473
22474 let multibuffer = cx.new(|cx| {
22475 let mut multibuffer = MultiBuffer::new(ReadWrite);
22476 multibuffer.set_excerpts_for_path(
22477 PathKey::sorted(0),
22478 buffer_1.clone(),
22479 [
22480 Point::new(0, 0)..Point::new(2, 0),
22481 Point::new(5, 0)..Point::new(6, 0),
22482 Point::new(9, 0)..Point::new(9, 4),
22483 ],
22484 0,
22485 cx,
22486 );
22487 multibuffer.set_excerpts_for_path(
22488 PathKey::sorted(1),
22489 buffer_2.clone(),
22490 [
22491 Point::new(0, 0)..Point::new(2, 0),
22492 Point::new(5, 0)..Point::new(6, 0),
22493 Point::new(9, 0)..Point::new(9, 4),
22494 ],
22495 0,
22496 cx,
22497 );
22498 multibuffer.set_excerpts_for_path(
22499 PathKey::sorted(2),
22500 buffer_3.clone(),
22501 [
22502 Point::new(0, 0)..Point::new(2, 0),
22503 Point::new(5, 0)..Point::new(6, 0),
22504 Point::new(9, 0)..Point::new(9, 4),
22505 ],
22506 0,
22507 cx,
22508 );
22509 multibuffer
22510 });
22511
22512 let fs = FakeFs::new(cx.executor());
22513 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22514 let (editor, cx) = cx
22515 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
22516 editor.update_in(cx, |editor, _window, cx| {
22517 for (buffer, diff_base) in [
22518 (buffer_1.clone(), base_text_1),
22519 (buffer_2.clone(), base_text_2),
22520 (buffer_3.clone(), base_text_3),
22521 ] {
22522 let diff = cx.new(|cx| {
22523 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
22524 });
22525 editor
22526 .buffer
22527 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
22528 }
22529 });
22530 cx.executor().run_until_parked();
22531
22532 editor.update_in(cx, |editor, window, cx| {
22533 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}");
22534 editor.select_all(&SelectAll, window, cx);
22535 editor.git_restore(&Default::default(), window, cx);
22536 });
22537 cx.executor().run_until_parked();
22538
22539 // When all ranges are selected, all buffer hunks are reverted.
22540 editor.update(cx, |editor, cx| {
22541 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");
22542 });
22543 buffer_1.update(cx, |buffer, _| {
22544 assert_eq!(buffer.text(), base_text_1);
22545 });
22546 buffer_2.update(cx, |buffer, _| {
22547 assert_eq!(buffer.text(), base_text_2);
22548 });
22549 buffer_3.update(cx, |buffer, _| {
22550 assert_eq!(buffer.text(), base_text_3);
22551 });
22552
22553 editor.update_in(cx, |editor, window, cx| {
22554 editor.undo(&Default::default(), window, cx);
22555 });
22556
22557 editor.update_in(cx, |editor, window, cx| {
22558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22559 s.select_ranges(Some(Point::new(0, 0)..Point::new(5, 0)));
22560 });
22561 editor.git_restore(&Default::default(), window, cx);
22562 });
22563
22564 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
22565 // but not affect buffer_2 and its related excerpts.
22566 editor.update(cx, |editor, cx| {
22567 assert_eq!(
22568 editor.display_text(cx),
22569 "\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}"
22570 );
22571 });
22572 buffer_1.update(cx, |buffer, _| {
22573 assert_eq!(buffer.text(), base_text_1);
22574 });
22575 buffer_2.update(cx, |buffer, _| {
22576 assert_eq!(
22577 buffer.text(),
22578 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
22579 );
22580 });
22581 buffer_3.update(cx, |buffer, _| {
22582 assert_eq!(
22583 buffer.text(),
22584 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
22585 );
22586 });
22587
22588 fn edit_first_char_of_every_line(text: &str) -> String {
22589 text.split('\n')
22590 .map(|line| format!("X{}", &line[1..]))
22591 .collect::<Vec<_>>()
22592 .join("\n")
22593 }
22594}
22595
22596#[gpui::test]
22597async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
22598 init_test(cx, |_| {});
22599
22600 let cols = 4;
22601 let rows = 10;
22602 let sample_text_1 = sample_text(rows, cols, 'a');
22603 assert_eq!(
22604 sample_text_1,
22605 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
22606 );
22607 let sample_text_2 = sample_text(rows, cols, 'l');
22608 assert_eq!(
22609 sample_text_2,
22610 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
22611 );
22612 let sample_text_3 = sample_text(rows, cols, 'v');
22613 assert_eq!(
22614 sample_text_3,
22615 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
22616 );
22617
22618 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
22619 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
22620 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
22621
22622 let multi_buffer = cx.new(|cx| {
22623 let mut multibuffer = MultiBuffer::new(ReadWrite);
22624 multibuffer.set_excerpts_for_path(
22625 PathKey::sorted(0),
22626 buffer_1.clone(),
22627 [
22628 Point::new(0, 0)..Point::new(2, 0),
22629 Point::new(5, 0)..Point::new(6, 0),
22630 Point::new(9, 0)..Point::new(9, 4),
22631 ],
22632 0,
22633 cx,
22634 );
22635 multibuffer.set_excerpts_for_path(
22636 PathKey::sorted(1),
22637 buffer_2.clone(),
22638 [
22639 Point::new(0, 0)..Point::new(2, 0),
22640 Point::new(5, 0)..Point::new(6, 0),
22641 Point::new(9, 0)..Point::new(9, 4),
22642 ],
22643 0,
22644 cx,
22645 );
22646 multibuffer.set_excerpts_for_path(
22647 PathKey::sorted(2),
22648 buffer_3.clone(),
22649 [
22650 Point::new(0, 0)..Point::new(2, 0),
22651 Point::new(5, 0)..Point::new(6, 0),
22652 Point::new(9, 0)..Point::new(9, 4),
22653 ],
22654 0,
22655 cx,
22656 );
22657 multibuffer
22658 });
22659
22660 let fs = FakeFs::new(cx.executor());
22661 fs.insert_tree(
22662 "/a",
22663 json!({
22664 "main.rs": sample_text_1,
22665 "other.rs": sample_text_2,
22666 "lib.rs": sample_text_3,
22667 }),
22668 )
22669 .await;
22670 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22671 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
22672 let workspace = window
22673 .read_with(cx, |mw, _| mw.workspace().clone())
22674 .unwrap();
22675 let cx = &mut VisualTestContext::from_window(*window, cx);
22676 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22677 Editor::new(
22678 EditorMode::full(),
22679 multi_buffer,
22680 Some(project.clone()),
22681 window,
22682 cx,
22683 )
22684 });
22685 let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| {
22686 assert!(
22687 workspace.active_item(cx).is_none(),
22688 "active item should be None before the first item is added"
22689 );
22690 workspace.add_item_to_active_pane(
22691 Box::new(multi_buffer_editor.clone()),
22692 None,
22693 true,
22694 window,
22695 cx,
22696 );
22697 let active_item = workspace
22698 .active_item(cx)
22699 .expect("should have an active item after adding the multi buffer");
22700 assert_eq!(
22701 active_item.buffer_kind(cx),
22702 ItemBufferKind::Multibuffer,
22703 "A multi buffer was expected to active after adding"
22704 );
22705 active_item.item_id()
22706 });
22707
22708 cx.executor().run_until_parked();
22709
22710 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22711 editor.change_selections(
22712 SelectionEffects::scroll(Autoscroll::Next),
22713 window,
22714 cx,
22715 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
22716 );
22717 editor.open_excerpts(&OpenExcerpts, window, cx);
22718 });
22719 cx.executor().run_until_parked();
22720 let first_item_id = workspace.update_in(cx, |workspace, window, cx| {
22721 let active_item = workspace
22722 .active_item(cx)
22723 .expect("should have an active item after navigating into the 1st buffer");
22724 let first_item_id = active_item.item_id();
22725 assert_ne!(
22726 first_item_id, multibuffer_item_id,
22727 "Should navigate into the 1st buffer and activate it"
22728 );
22729 assert_eq!(
22730 active_item.buffer_kind(cx),
22731 ItemBufferKind::Singleton,
22732 "New active item should be a singleton buffer"
22733 );
22734 assert_eq!(
22735 active_item
22736 .act_as::<Editor>(cx)
22737 .expect("should have navigated into an editor for the 1st buffer")
22738 .read(cx)
22739 .text(cx),
22740 sample_text_1
22741 );
22742
22743 workspace
22744 .go_back(workspace.active_pane().downgrade(), window, cx)
22745 .detach_and_log_err(cx);
22746
22747 first_item_id
22748 });
22749
22750 cx.executor().run_until_parked();
22751 workspace.update_in(cx, |workspace, _, cx| {
22752 let active_item = workspace
22753 .active_item(cx)
22754 .expect("should have an active item after navigating back");
22755 assert_eq!(
22756 active_item.item_id(),
22757 multibuffer_item_id,
22758 "Should navigate back to the multi buffer"
22759 );
22760 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22761 });
22762
22763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22764 editor.change_selections(
22765 SelectionEffects::scroll(Autoscroll::Next),
22766 window,
22767 cx,
22768 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
22769 );
22770 editor.open_excerpts(&OpenExcerpts, window, cx);
22771 });
22772 cx.executor().run_until_parked();
22773 let second_item_id = workspace.update_in(cx, |workspace, window, cx| {
22774 let active_item = workspace
22775 .active_item(cx)
22776 .expect("should have an active item after navigating into the 2nd buffer");
22777 let second_item_id = active_item.item_id();
22778 assert_ne!(
22779 second_item_id, multibuffer_item_id,
22780 "Should navigate away from the multibuffer"
22781 );
22782 assert_ne!(
22783 second_item_id, first_item_id,
22784 "Should navigate into the 2nd buffer and activate it"
22785 );
22786 assert_eq!(
22787 active_item.buffer_kind(cx),
22788 ItemBufferKind::Singleton,
22789 "New active item should be a singleton buffer"
22790 );
22791 assert_eq!(
22792 active_item
22793 .act_as::<Editor>(cx)
22794 .expect("should have navigated into an editor")
22795 .read(cx)
22796 .text(cx),
22797 sample_text_2
22798 );
22799
22800 workspace
22801 .go_back(workspace.active_pane().downgrade(), window, cx)
22802 .detach_and_log_err(cx);
22803
22804 second_item_id
22805 });
22806
22807 cx.executor().run_until_parked();
22808 workspace.update_in(cx, |workspace, _, cx| {
22809 let active_item = workspace
22810 .active_item(cx)
22811 .expect("should have an active item after navigating back from the 2nd buffer");
22812 assert_eq!(
22813 active_item.item_id(),
22814 multibuffer_item_id,
22815 "Should navigate back from the 2nd buffer to the multi buffer"
22816 );
22817 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22818 });
22819
22820 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22821 editor.change_selections(
22822 SelectionEffects::scroll(Autoscroll::Next),
22823 window,
22824 cx,
22825 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
22826 );
22827 editor.open_excerpts(&OpenExcerpts, window, cx);
22828 });
22829 cx.executor().run_until_parked();
22830 workspace.update_in(cx, |workspace, window, cx| {
22831 let active_item = workspace
22832 .active_item(cx)
22833 .expect("should have an active item after navigating into the 3rd buffer");
22834 let third_item_id = active_item.item_id();
22835 assert_ne!(
22836 third_item_id, multibuffer_item_id,
22837 "Should navigate into the 3rd buffer and activate it"
22838 );
22839 assert_ne!(third_item_id, first_item_id);
22840 assert_ne!(third_item_id, second_item_id);
22841 assert_eq!(
22842 active_item.buffer_kind(cx),
22843 ItemBufferKind::Singleton,
22844 "New active item should be a singleton buffer"
22845 );
22846 assert_eq!(
22847 active_item
22848 .act_as::<Editor>(cx)
22849 .expect("should have navigated into an editor")
22850 .read(cx)
22851 .text(cx),
22852 sample_text_3
22853 );
22854
22855 workspace
22856 .go_back(workspace.active_pane().downgrade(), window, cx)
22857 .detach_and_log_err(cx);
22858 });
22859
22860 cx.executor().run_until_parked();
22861 workspace.update_in(cx, |workspace, _, cx| {
22862 let active_item = workspace
22863 .active_item(cx)
22864 .expect("should have an active item after navigating back from the 3rd buffer");
22865 assert_eq!(
22866 active_item.item_id(),
22867 multibuffer_item_id,
22868 "Should navigate back from the 3rd buffer to the multi buffer"
22869 );
22870 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
22871 });
22872}
22873
22874#[gpui::test]
22875async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22876 init_test(cx, |_| {});
22877
22878 let mut cx = EditorTestContext::new(cx).await;
22879
22880 let diff_base = r#"
22881 use some::mod;
22882
22883 const A: u32 = 42;
22884
22885 fn main() {
22886 println!("hello");
22887
22888 println!("world");
22889 }
22890 "#
22891 .unindent();
22892
22893 cx.set_state(
22894 &r#"
22895 use some::modified;
22896
22897 ˇ
22898 fn main() {
22899 println!("hello there");
22900
22901 println!("around the");
22902 println!("world");
22903 }
22904 "#
22905 .unindent(),
22906 );
22907
22908 cx.set_head_text(&diff_base);
22909 executor.run_until_parked();
22910
22911 cx.update_editor(|editor, window, cx| {
22912 editor.go_to_next_hunk(&GoToHunk, window, cx);
22913 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22914 });
22915 executor.run_until_parked();
22916 cx.assert_state_with_diff(
22917 r#"
22918 use some::modified;
22919
22920
22921 fn main() {
22922 - println!("hello");
22923 + ˇ println!("hello there");
22924
22925 println!("around the");
22926 println!("world");
22927 }
22928 "#
22929 .unindent(),
22930 );
22931
22932 cx.update_editor(|editor, window, cx| {
22933 for _ in 0..2 {
22934 editor.go_to_next_hunk(&GoToHunk, window, cx);
22935 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22936 }
22937 });
22938 executor.run_until_parked();
22939 cx.assert_state_with_diff(
22940 r#"
22941 - use some::mod;
22942 + ˇuse some::modified;
22943
22944
22945 fn main() {
22946 - println!("hello");
22947 + println!("hello there");
22948
22949 + println!("around the");
22950 println!("world");
22951 }
22952 "#
22953 .unindent(),
22954 );
22955
22956 cx.update_editor(|editor, window, cx| {
22957 editor.go_to_next_hunk(&GoToHunk, window, cx);
22958 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22959 });
22960 executor.run_until_parked();
22961 cx.assert_state_with_diff(
22962 r#"
22963 - use some::mod;
22964 + use some::modified;
22965
22966 - const A: u32 = 42;
22967 ˇ
22968 fn main() {
22969 - println!("hello");
22970 + println!("hello there");
22971
22972 + println!("around the");
22973 println!("world");
22974 }
22975 "#
22976 .unindent(),
22977 );
22978
22979 cx.update_editor(|editor, window, cx| {
22980 editor.cancel(&Cancel, window, cx);
22981 });
22982
22983 cx.assert_state_with_diff(
22984 r#"
22985 use some::modified;
22986
22987 ˇ
22988 fn main() {
22989 println!("hello there");
22990
22991 println!("around the");
22992 println!("world");
22993 }
22994 "#
22995 .unindent(),
22996 );
22997}
22998
22999#[gpui::test]
23000async fn test_diff_base_change_with_expanded_diff_hunks(
23001 executor: BackgroundExecutor,
23002 cx: &mut TestAppContext,
23003) {
23004 init_test(cx, |_| {});
23005
23006 let mut cx = EditorTestContext::new(cx).await;
23007
23008 let diff_base = r#"
23009 use some::mod1;
23010 use some::mod2;
23011
23012 const A: u32 = 42;
23013 const B: u32 = 42;
23014 const C: u32 = 42;
23015
23016 fn main() {
23017 println!("hello");
23018
23019 println!("world");
23020 }
23021 "#
23022 .unindent();
23023
23024 cx.set_state(
23025 &r#"
23026 use some::mod2;
23027
23028 const A: u32 = 42;
23029 const C: u32 = 42;
23030
23031 fn main(ˇ) {
23032 //println!("hello");
23033
23034 println!("world");
23035 //
23036 //
23037 }
23038 "#
23039 .unindent(),
23040 );
23041
23042 cx.set_head_text(&diff_base);
23043 executor.run_until_parked();
23044
23045 cx.update_editor(|editor, window, cx| {
23046 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23047 });
23048 executor.run_until_parked();
23049 cx.assert_state_with_diff(
23050 r#"
23051 - use some::mod1;
23052 use some::mod2;
23053
23054 const A: u32 = 42;
23055 - const B: u32 = 42;
23056 const C: u32 = 42;
23057
23058 fn main(ˇ) {
23059 - println!("hello");
23060 + //println!("hello");
23061
23062 println!("world");
23063 + //
23064 + //
23065 }
23066 "#
23067 .unindent(),
23068 );
23069
23070 cx.set_head_text("new diff base!");
23071 executor.run_until_parked();
23072 cx.assert_state_with_diff(
23073 r#"
23074 - new diff base!
23075 + use some::mod2;
23076 +
23077 + const A: u32 = 42;
23078 + const C: u32 = 42;
23079 +
23080 + fn main(ˇ) {
23081 + //println!("hello");
23082 +
23083 + println!("world");
23084 + //
23085 + //
23086 + }
23087 "#
23088 .unindent(),
23089 );
23090}
23091
23092#[gpui::test]
23093async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
23094 init_test(cx, |_| {});
23095
23096 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
23097 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
23098 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
23099 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
23100 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
23101 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
23102
23103 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
23104 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
23105 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
23106
23107 let multi_buffer = cx.new(|cx| {
23108 let mut multibuffer = MultiBuffer::new(ReadWrite);
23109 multibuffer.set_excerpts_for_path(
23110 PathKey::sorted(0),
23111 buffer_1.clone(),
23112 [
23113 Point::new(0, 0)..Point::new(2, 3),
23114 Point::new(5, 0)..Point::new(6, 3),
23115 Point::new(9, 0)..Point::new(10, 3),
23116 ],
23117 0,
23118 cx,
23119 );
23120 multibuffer.set_excerpts_for_path(
23121 PathKey::sorted(1),
23122 buffer_2.clone(),
23123 [
23124 Point::new(0, 0)..Point::new(2, 3),
23125 Point::new(5, 0)..Point::new(6, 3),
23126 Point::new(9, 0)..Point::new(10, 3),
23127 ],
23128 0,
23129 cx,
23130 );
23131 multibuffer.set_excerpts_for_path(
23132 PathKey::sorted(2),
23133 buffer_3.clone(),
23134 [
23135 Point::new(0, 0)..Point::new(2, 3),
23136 Point::new(5, 0)..Point::new(6, 3),
23137 Point::new(9, 0)..Point::new(10, 3),
23138 ],
23139 0,
23140 cx,
23141 );
23142 assert_eq!(multibuffer.read(cx).excerpts().count(), 9);
23143 multibuffer
23144 });
23145
23146 let editor =
23147 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
23148 editor
23149 .update(cx, |editor, _window, cx| {
23150 for (buffer, diff_base) in [
23151 (buffer_1.clone(), file_1_old),
23152 (buffer_2.clone(), file_2_old),
23153 (buffer_3.clone(), file_3_old),
23154 ] {
23155 let diff = cx.new(|cx| {
23156 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
23157 });
23158 editor
23159 .buffer
23160 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
23161 }
23162 })
23163 .unwrap();
23164
23165 let mut cx = EditorTestContext::for_editor(editor, cx).await;
23166 cx.run_until_parked();
23167
23168 cx.assert_editor_state(
23169 &"
23170 ˇaaa
23171 ccc
23172 ddd
23173 ggg
23174 hhh
23175
23176 lll
23177 mmm
23178 NNN
23179 qqq
23180 rrr
23181 uuu
23182 111
23183 222
23184 333
23185 666
23186 777
23187 000
23188 !!!"
23189 .unindent(),
23190 );
23191
23192 cx.update_editor(|editor, window, cx| {
23193 editor.select_all(&SelectAll, window, cx);
23194 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23195 });
23196 cx.executor().run_until_parked();
23197
23198 cx.assert_state_with_diff(
23199 "
23200 «aaa
23201 - bbb
23202 ccc
23203 ddd
23204 ggg
23205 hhh
23206
23207 lll
23208 mmm
23209 - nnn
23210 + NNN
23211 qqq
23212 rrr
23213 uuu
23214 111
23215 222
23216 333
23217 + 666
23218 777
23219 000
23220 !!!ˇ»"
23221 .unindent(),
23222 );
23223}
23224
23225#[gpui::test]
23226async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
23227 init_test(cx, |_| {});
23228
23229 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
23230 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
23231
23232 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
23233 let multi_buffer = cx.new(|cx| {
23234 let mut multibuffer = MultiBuffer::new(ReadWrite);
23235 multibuffer.set_excerpts_for_path(
23236 PathKey::sorted(0),
23237 buffer.clone(),
23238 [
23239 Point::new(0, 0)..Point::new(1, 3),
23240 Point::new(4, 0)..Point::new(6, 3),
23241 Point::new(9, 0)..Point::new(9, 3),
23242 ],
23243 0,
23244 cx,
23245 );
23246 assert_eq!(multibuffer.read(cx).excerpts().count(), 3);
23247 multibuffer
23248 });
23249
23250 let editor =
23251 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
23252 editor
23253 .update(cx, |editor, _window, cx| {
23254 let diff = cx.new(|cx| {
23255 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
23256 });
23257 editor
23258 .buffer
23259 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
23260 })
23261 .unwrap();
23262
23263 let mut cx = EditorTestContext::for_editor(editor, cx).await;
23264 cx.run_until_parked();
23265
23266 cx.update_editor(|editor, window, cx| {
23267 editor.expand_all_diff_hunks(&Default::default(), window, cx)
23268 });
23269 cx.executor().run_until_parked();
23270
23271 // When the start of a hunk coincides with the start of its excerpt,
23272 // the hunk is expanded. When the start of a hunk is earlier than
23273 // the start of its excerpt, the hunk is not expanded.
23274 cx.assert_state_with_diff(
23275 "
23276 ˇaaa
23277 - bbb
23278 + BBB
23279 - ddd
23280 - eee
23281 + DDD
23282 + EEE
23283 fff
23284 iii"
23285 .unindent(),
23286 );
23287}
23288
23289#[gpui::test]
23290async fn test_edits_around_expanded_insertion_hunks(
23291 executor: BackgroundExecutor,
23292 cx: &mut TestAppContext,
23293) {
23294 init_test(cx, |_| {});
23295
23296 let mut cx = EditorTestContext::new(cx).await;
23297
23298 let diff_base = r#"
23299 use some::mod1;
23300 use some::mod2;
23301
23302 const A: u32 = 42;
23303
23304 fn main() {
23305 println!("hello");
23306
23307 println!("world");
23308 }
23309 "#
23310 .unindent();
23311 executor.run_until_parked();
23312 cx.set_state(
23313 &r#"
23314 use some::mod1;
23315 use some::mod2;
23316
23317 const A: u32 = 42;
23318 const B: u32 = 42;
23319 const C: u32 = 42;
23320 ˇ
23321
23322 fn main() {
23323 println!("hello");
23324
23325 println!("world");
23326 }
23327 "#
23328 .unindent(),
23329 );
23330
23331 cx.set_head_text(&diff_base);
23332 executor.run_until_parked();
23333
23334 cx.update_editor(|editor, window, cx| {
23335 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23336 });
23337 executor.run_until_parked();
23338
23339 cx.assert_state_with_diff(
23340 r#"
23341 use some::mod1;
23342 use some::mod2;
23343
23344 const A: u32 = 42;
23345 + const B: u32 = 42;
23346 + const C: u32 = 42;
23347 + ˇ
23348
23349 fn main() {
23350 println!("hello");
23351
23352 println!("world");
23353 }
23354 "#
23355 .unindent(),
23356 );
23357
23358 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
23359 executor.run_until_parked();
23360
23361 cx.assert_state_with_diff(
23362 r#"
23363 use some::mod1;
23364 use some::mod2;
23365
23366 const A: u32 = 42;
23367 + const B: u32 = 42;
23368 + const C: u32 = 42;
23369 + const D: u32 = 42;
23370 + ˇ
23371
23372 fn main() {
23373 println!("hello");
23374
23375 println!("world");
23376 }
23377 "#
23378 .unindent(),
23379 );
23380
23381 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
23382 executor.run_until_parked();
23383
23384 cx.assert_state_with_diff(
23385 r#"
23386 use some::mod1;
23387 use some::mod2;
23388
23389 const A: u32 = 42;
23390 + const B: u32 = 42;
23391 + const C: u32 = 42;
23392 + const D: u32 = 42;
23393 + const E: u32 = 42;
23394 + ˇ
23395
23396 fn main() {
23397 println!("hello");
23398
23399 println!("world");
23400 }
23401 "#
23402 .unindent(),
23403 );
23404
23405 cx.update_editor(|editor, window, cx| {
23406 editor.delete_line(&DeleteLine, window, cx);
23407 });
23408 executor.run_until_parked();
23409
23410 cx.assert_state_with_diff(
23411 r#"
23412 use some::mod1;
23413 use some::mod2;
23414
23415 const A: u32 = 42;
23416 + const B: u32 = 42;
23417 + const C: u32 = 42;
23418 + const D: u32 = 42;
23419 + const E: u32 = 42;
23420 ˇ
23421 fn main() {
23422 println!("hello");
23423
23424 println!("world");
23425 }
23426 "#
23427 .unindent(),
23428 );
23429
23430 cx.update_editor(|editor, window, cx| {
23431 editor.move_up(&MoveUp, window, cx);
23432 editor.delete_line(&DeleteLine, window, cx);
23433 editor.move_up(&MoveUp, window, cx);
23434 editor.delete_line(&DeleteLine, window, cx);
23435 editor.move_up(&MoveUp, window, cx);
23436 editor.delete_line(&DeleteLine, window, cx);
23437 });
23438 executor.run_until_parked();
23439 cx.assert_state_with_diff(
23440 r#"
23441 use some::mod1;
23442 use some::mod2;
23443
23444 const A: u32 = 42;
23445 + const B: u32 = 42;
23446 ˇ
23447 fn main() {
23448 println!("hello");
23449
23450 println!("world");
23451 }
23452 "#
23453 .unindent(),
23454 );
23455
23456 cx.update_editor(|editor, window, cx| {
23457 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
23458 editor.delete_line(&DeleteLine, window, cx);
23459 });
23460 executor.run_until_parked();
23461 cx.assert_state_with_diff(
23462 r#"
23463 ˇ
23464 fn main() {
23465 println!("hello");
23466
23467 println!("world");
23468 }
23469 "#
23470 .unindent(),
23471 );
23472}
23473
23474#[gpui::test]
23475async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
23476 init_test(cx, |_| {});
23477
23478 let mut cx = EditorTestContext::new(cx).await;
23479 cx.set_head_text(indoc! { "
23480 one
23481 two
23482 three
23483 four
23484 five
23485 "
23486 });
23487 cx.set_state(indoc! { "
23488 one
23489 ˇthree
23490 five
23491 "});
23492 cx.run_until_parked();
23493 cx.update_editor(|editor, window, cx| {
23494 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23495 });
23496 cx.assert_state_with_diff(
23497 indoc! { "
23498 one
23499 - two
23500 ˇthree
23501 - four
23502 five
23503 "}
23504 .to_string(),
23505 );
23506 cx.update_editor(|editor, window, cx| {
23507 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23508 });
23509
23510 cx.assert_state_with_diff(
23511 indoc! { "
23512 one
23513 ˇthree
23514 five
23515 "}
23516 .to_string(),
23517 );
23518
23519 cx.update_editor(|editor, window, cx| {
23520 editor.move_up(&MoveUp, window, cx);
23521 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23522 });
23523 cx.assert_state_with_diff(
23524 indoc! { "
23525 ˇone
23526 - two
23527 three
23528 five
23529 "}
23530 .to_string(),
23531 );
23532
23533 cx.update_editor(|editor, window, cx| {
23534 editor.move_down(&MoveDown, window, cx);
23535 editor.move_down(&MoveDown, window, cx);
23536 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23537 });
23538 cx.assert_state_with_diff(
23539 indoc! { "
23540 one
23541 - two
23542 ˇthree
23543 - four
23544 five
23545 "}
23546 .to_string(),
23547 );
23548
23549 cx.set_state(indoc! { "
23550 one
23551 ˇTWO
23552 three
23553 four
23554 five
23555 "});
23556 cx.run_until_parked();
23557 cx.update_editor(|editor, window, cx| {
23558 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23559 });
23560
23561 cx.assert_state_with_diff(
23562 indoc! { "
23563 one
23564 - two
23565 + ˇTWO
23566 three
23567 four
23568 five
23569 "}
23570 .to_string(),
23571 );
23572 cx.update_editor(|editor, window, cx| {
23573 editor.move_up(&Default::default(), window, cx);
23574 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
23575 });
23576 cx.assert_state_with_diff(
23577 indoc! { "
23578 one
23579 ˇTWO
23580 three
23581 four
23582 five
23583 "}
23584 .to_string(),
23585 );
23586}
23587
23588#[gpui::test]
23589async fn test_toggling_adjacent_diff_hunks_2(
23590 executor: BackgroundExecutor,
23591 cx: &mut TestAppContext,
23592) {
23593 init_test(cx, |_| {});
23594
23595 let mut cx = EditorTestContext::new(cx).await;
23596
23597 let diff_base = r#"
23598 lineA
23599 lineB
23600 lineC
23601 lineD
23602 "#
23603 .unindent();
23604
23605 cx.set_state(
23606 &r#"
23607 ˇlineA1
23608 lineB
23609 lineD
23610 "#
23611 .unindent(),
23612 );
23613 cx.set_head_text(&diff_base);
23614 executor.run_until_parked();
23615
23616 cx.update_editor(|editor, window, cx| {
23617 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23618 });
23619 executor.run_until_parked();
23620 cx.assert_state_with_diff(
23621 r#"
23622 - lineA
23623 + ˇlineA1
23624 lineB
23625 lineD
23626 "#
23627 .unindent(),
23628 );
23629
23630 cx.update_editor(|editor, window, cx| {
23631 editor.move_down(&MoveDown, window, cx);
23632 editor.move_right(&MoveRight, window, cx);
23633 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
23634 });
23635 executor.run_until_parked();
23636 cx.assert_state_with_diff(
23637 r#"
23638 - lineA
23639 + lineA1
23640 lˇineB
23641 - lineC
23642 lineD
23643 "#
23644 .unindent(),
23645 );
23646}
23647
23648#[gpui::test]
23649async fn test_edits_around_expanded_deletion_hunks(
23650 executor: BackgroundExecutor,
23651 cx: &mut TestAppContext,
23652) {
23653 init_test(cx, |_| {});
23654
23655 let mut cx = EditorTestContext::new(cx).await;
23656
23657 let diff_base = r#"
23658 use some::mod1;
23659 use some::mod2;
23660
23661 const A: u32 = 42;
23662 const B: u32 = 42;
23663 const C: u32 = 42;
23664
23665
23666 fn main() {
23667 println!("hello");
23668
23669 println!("world");
23670 }
23671 "#
23672 .unindent();
23673 executor.run_until_parked();
23674 cx.set_state(
23675 &r#"
23676 use some::mod1;
23677 use some::mod2;
23678
23679 ˇconst B: u32 = 42;
23680 const C: u32 = 42;
23681
23682
23683 fn main() {
23684 println!("hello");
23685
23686 println!("world");
23687 }
23688 "#
23689 .unindent(),
23690 );
23691
23692 cx.set_head_text(&diff_base);
23693 executor.run_until_parked();
23694
23695 cx.update_editor(|editor, window, cx| {
23696 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23697 });
23698 executor.run_until_parked();
23699
23700 cx.assert_state_with_diff(
23701 r#"
23702 use some::mod1;
23703 use some::mod2;
23704
23705 - const A: u32 = 42;
23706 ˇconst B: u32 = 42;
23707 const C: u32 = 42;
23708
23709
23710 fn main() {
23711 println!("hello");
23712
23713 println!("world");
23714 }
23715 "#
23716 .unindent(),
23717 );
23718
23719 cx.update_editor(|editor, window, cx| {
23720 editor.delete_line(&DeleteLine, window, cx);
23721 });
23722 executor.run_until_parked();
23723 cx.assert_state_with_diff(
23724 r#"
23725 use some::mod1;
23726 use some::mod2;
23727
23728 - const A: u32 = 42;
23729 - const B: u32 = 42;
23730 ˇconst C: u32 = 42;
23731
23732
23733 fn main() {
23734 println!("hello");
23735
23736 println!("world");
23737 }
23738 "#
23739 .unindent(),
23740 );
23741
23742 cx.update_editor(|editor, window, cx| {
23743 editor.delete_line(&DeleteLine, window, cx);
23744 });
23745 executor.run_until_parked();
23746 cx.assert_state_with_diff(
23747 r#"
23748 use some::mod1;
23749 use some::mod2;
23750
23751 - const A: u32 = 42;
23752 - const B: u32 = 42;
23753 - const C: u32 = 42;
23754 ˇ
23755
23756 fn main() {
23757 println!("hello");
23758
23759 println!("world");
23760 }
23761 "#
23762 .unindent(),
23763 );
23764
23765 cx.update_editor(|editor, window, cx| {
23766 editor.handle_input("replacement", window, cx);
23767 });
23768 executor.run_until_parked();
23769 cx.assert_state_with_diff(
23770 r#"
23771 use some::mod1;
23772 use some::mod2;
23773
23774 - const A: u32 = 42;
23775 - const B: u32 = 42;
23776 - const C: u32 = 42;
23777 -
23778 + replacementˇ
23779
23780 fn main() {
23781 println!("hello");
23782
23783 println!("world");
23784 }
23785 "#
23786 .unindent(),
23787 );
23788}
23789
23790#[gpui::test]
23791async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
23792 init_test(cx, |_| {});
23793
23794 let mut cx = EditorTestContext::new(cx).await;
23795
23796 let base_text = r#"
23797 one
23798 two
23799 three
23800 four
23801 five
23802 "#
23803 .unindent();
23804 executor.run_until_parked();
23805 cx.set_state(
23806 &r#"
23807 one
23808 two
23809 fˇour
23810 five
23811 "#
23812 .unindent(),
23813 );
23814
23815 cx.set_head_text(&base_text);
23816 executor.run_until_parked();
23817
23818 cx.update_editor(|editor, window, cx| {
23819 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23820 });
23821 executor.run_until_parked();
23822
23823 cx.assert_state_with_diff(
23824 r#"
23825 one
23826 two
23827 - three
23828 fˇour
23829 five
23830 "#
23831 .unindent(),
23832 );
23833
23834 cx.update_editor(|editor, window, cx| {
23835 editor.backspace(&Backspace, window, cx);
23836 editor.backspace(&Backspace, window, cx);
23837 });
23838 executor.run_until_parked();
23839 cx.assert_state_with_diff(
23840 r#"
23841 one
23842 two
23843 - threeˇ
23844 - four
23845 + our
23846 five
23847 "#
23848 .unindent(),
23849 );
23850}
23851
23852#[gpui::test]
23853async fn test_edit_after_expanded_modification_hunk(
23854 executor: BackgroundExecutor,
23855 cx: &mut TestAppContext,
23856) {
23857 init_test(cx, |_| {});
23858
23859 let mut cx = EditorTestContext::new(cx).await;
23860
23861 let diff_base = r#"
23862 use some::mod1;
23863 use some::mod2;
23864
23865 const A: u32 = 42;
23866 const B: u32 = 42;
23867 const C: u32 = 42;
23868 const D: u32 = 42;
23869
23870
23871 fn main() {
23872 println!("hello");
23873
23874 println!("world");
23875 }"#
23876 .unindent();
23877
23878 cx.set_state(
23879 &r#"
23880 use some::mod1;
23881 use some::mod2;
23882
23883 const A: u32 = 42;
23884 const B: u32 = 42;
23885 const C: u32 = 43ˇ
23886 const D: u32 = 42;
23887
23888
23889 fn main() {
23890 println!("hello");
23891
23892 println!("world");
23893 }"#
23894 .unindent(),
23895 );
23896
23897 cx.set_head_text(&diff_base);
23898 executor.run_until_parked();
23899 cx.update_editor(|editor, window, cx| {
23900 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23901 });
23902 executor.run_until_parked();
23903
23904 cx.assert_state_with_diff(
23905 r#"
23906 use some::mod1;
23907 use some::mod2;
23908
23909 const A: u32 = 42;
23910 const B: u32 = 42;
23911 - const C: u32 = 42;
23912 + const C: u32 = 43ˇ
23913 const D: u32 = 42;
23914
23915
23916 fn main() {
23917 println!("hello");
23918
23919 println!("world");
23920 }"#
23921 .unindent(),
23922 );
23923
23924 cx.update_editor(|editor, window, cx| {
23925 editor.handle_input("\nnew_line\n", window, cx);
23926 });
23927 executor.run_until_parked();
23928
23929 cx.assert_state_with_diff(
23930 r#"
23931 use some::mod1;
23932 use some::mod2;
23933
23934 const A: u32 = 42;
23935 const B: u32 = 42;
23936 - const C: u32 = 42;
23937 + const C: u32 = 43
23938 + new_line
23939 + ˇ
23940 const D: u32 = 42;
23941
23942
23943 fn main() {
23944 println!("hello");
23945
23946 println!("world");
23947 }"#
23948 .unindent(),
23949 );
23950}
23951
23952#[gpui::test]
23953async fn test_stage_and_unstage_added_file_hunk(
23954 executor: BackgroundExecutor,
23955 cx: &mut TestAppContext,
23956) {
23957 init_test(cx, |_| {});
23958
23959 let mut cx = EditorTestContext::new(cx).await;
23960 cx.update_editor(|editor, _, cx| {
23961 editor.set_expand_all_diff_hunks(cx);
23962 });
23963
23964 let working_copy = r#"
23965 ˇfn main() {
23966 println!("hello, world!");
23967 }
23968 "#
23969 .unindent();
23970
23971 cx.set_state(&working_copy);
23972 executor.run_until_parked();
23973
23974 cx.assert_state_with_diff(
23975 r#"
23976 + ˇfn main() {
23977 + println!("hello, world!");
23978 + }
23979 "#
23980 .unindent(),
23981 );
23982 cx.assert_index_text(None);
23983
23984 cx.update_editor(|editor, window, cx| {
23985 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23986 });
23987 executor.run_until_parked();
23988 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
23989 cx.assert_state_with_diff(
23990 r#"
23991 + ˇfn main() {
23992 + println!("hello, world!");
23993 + }
23994 "#
23995 .unindent(),
23996 );
23997
23998 cx.update_editor(|editor, window, cx| {
23999 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
24000 });
24001 executor.run_until_parked();
24002 cx.assert_index_text(None);
24003}
24004
24005async fn setup_indent_guides_editor(
24006 text: &str,
24007 cx: &mut TestAppContext,
24008) -> (BufferId, EditorTestContext) {
24009 init_test(cx, |_| {});
24010
24011 let mut cx = EditorTestContext::new(cx).await;
24012
24013 let buffer_id = cx.update_editor(|editor, window, cx| {
24014 editor.set_text(text, window, cx);
24015 editor
24016 .buffer()
24017 .read(cx)
24018 .as_singleton()
24019 .unwrap()
24020 .read(cx)
24021 .remote_id()
24022 });
24023
24024 (buffer_id, cx)
24025}
24026
24027fn assert_indent_guides(
24028 range: Range<u32>,
24029 expected: Vec<IndentGuide>,
24030 active_indices: Option<Vec<usize>>,
24031 cx: &mut EditorTestContext,
24032) {
24033 let indent_guides = cx.update_editor(|editor, window, cx| {
24034 let snapshot = editor.snapshot(window, cx).display_snapshot;
24035 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
24036 editor,
24037 MultiBufferRow(range.start)..MultiBufferRow(range.end),
24038 true,
24039 &snapshot,
24040 cx,
24041 );
24042
24043 indent_guides.sort_by(|a, b| {
24044 a.depth.cmp(&b.depth).then(
24045 a.start_row
24046 .cmp(&b.start_row)
24047 .then(a.end_row.cmp(&b.end_row)),
24048 )
24049 });
24050 indent_guides
24051 });
24052
24053 if let Some(expected) = active_indices {
24054 let active_indices = cx.update_editor(|editor, window, cx| {
24055 let snapshot = editor.snapshot(window, cx).display_snapshot;
24056 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
24057 });
24058
24059 assert_eq!(
24060 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
24061 expected,
24062 "Active indent guide indices do not match"
24063 );
24064 }
24065
24066 assert_eq!(indent_guides, expected, "Indent guides do not match");
24067}
24068
24069fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
24070 IndentGuide {
24071 buffer_id,
24072 start_row: MultiBufferRow(start_row),
24073 end_row: MultiBufferRow(end_row),
24074 depth,
24075 tab_size: 4,
24076 settings: IndentGuideSettings {
24077 enabled: true,
24078 line_width: 1,
24079 active_line_width: 1,
24080 coloring: IndentGuideColoring::default(),
24081 background_coloring: IndentGuideBackgroundColoring::default(),
24082 },
24083 }
24084}
24085
24086#[gpui::test]
24087async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
24088 let (buffer_id, mut cx) = setup_indent_guides_editor(
24089 &"
24090 fn main() {
24091 let a = 1;
24092 }"
24093 .unindent(),
24094 cx,
24095 )
24096 .await;
24097
24098 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
24099}
24100
24101#[gpui::test]
24102async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
24103 let (buffer_id, mut cx) = setup_indent_guides_editor(
24104 &"
24105 fn main() {
24106 let a = 1;
24107 let b = 2;
24108 }"
24109 .unindent(),
24110 cx,
24111 )
24112 .await;
24113
24114 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
24115}
24116
24117#[gpui::test]
24118async fn test_indent_guide_nested(cx: &mut TestAppContext) {
24119 let (buffer_id, mut cx) = setup_indent_guides_editor(
24120 &"
24121 fn main() {
24122 let a = 1;
24123 if a == 3 {
24124 let b = 2;
24125 } else {
24126 let c = 3;
24127 }
24128 }"
24129 .unindent(),
24130 cx,
24131 )
24132 .await;
24133
24134 assert_indent_guides(
24135 0..8,
24136 vec![
24137 indent_guide(buffer_id, 1, 6, 0),
24138 indent_guide(buffer_id, 3, 3, 1),
24139 indent_guide(buffer_id, 5, 5, 1),
24140 ],
24141 None,
24142 &mut cx,
24143 );
24144}
24145
24146#[gpui::test]
24147async fn test_indent_guide_tab(cx: &mut TestAppContext) {
24148 let (buffer_id, mut cx) = setup_indent_guides_editor(
24149 &"
24150 fn main() {
24151 let a = 1;
24152 let b = 2;
24153 let c = 3;
24154 }"
24155 .unindent(),
24156 cx,
24157 )
24158 .await;
24159
24160 assert_indent_guides(
24161 0..5,
24162 vec![
24163 indent_guide(buffer_id, 1, 3, 0),
24164 indent_guide(buffer_id, 2, 2, 1),
24165 ],
24166 None,
24167 &mut cx,
24168 );
24169}
24170
24171#[gpui::test]
24172async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
24173 let (buffer_id, mut cx) = setup_indent_guides_editor(
24174 &"
24175 fn main() {
24176 let a = 1;
24177
24178 let c = 3;
24179 }"
24180 .unindent(),
24181 cx,
24182 )
24183 .await;
24184
24185 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
24186}
24187
24188#[gpui::test]
24189async fn test_indent_guide_complex(cx: &mut TestAppContext) {
24190 let (buffer_id, mut cx) = setup_indent_guides_editor(
24191 &"
24192 fn main() {
24193 let a = 1;
24194
24195 let c = 3;
24196
24197 if a == 3 {
24198 let b = 2;
24199 } else {
24200 let c = 3;
24201 }
24202 }"
24203 .unindent(),
24204 cx,
24205 )
24206 .await;
24207
24208 assert_indent_guides(
24209 0..11,
24210 vec![
24211 indent_guide(buffer_id, 1, 9, 0),
24212 indent_guide(buffer_id, 6, 6, 1),
24213 indent_guide(buffer_id, 8, 8, 1),
24214 ],
24215 None,
24216 &mut cx,
24217 );
24218}
24219
24220#[gpui::test]
24221async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
24222 let (buffer_id, mut cx) = setup_indent_guides_editor(
24223 &"
24224 fn main() {
24225 let a = 1;
24226
24227 let c = 3;
24228
24229 if a == 3 {
24230 let b = 2;
24231 } else {
24232 let c = 3;
24233 }
24234 }"
24235 .unindent(),
24236 cx,
24237 )
24238 .await;
24239
24240 assert_indent_guides(
24241 1..11,
24242 vec![
24243 indent_guide(buffer_id, 1, 9, 0),
24244 indent_guide(buffer_id, 6, 6, 1),
24245 indent_guide(buffer_id, 8, 8, 1),
24246 ],
24247 None,
24248 &mut cx,
24249 );
24250}
24251
24252#[gpui::test]
24253async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
24254 let (buffer_id, mut cx) = setup_indent_guides_editor(
24255 &"
24256 fn main() {
24257 let a = 1;
24258
24259 let c = 3;
24260
24261 if a == 3 {
24262 let b = 2;
24263 } else {
24264 let c = 3;
24265 }
24266 }"
24267 .unindent(),
24268 cx,
24269 )
24270 .await;
24271
24272 assert_indent_guides(
24273 1..10,
24274 vec![
24275 indent_guide(buffer_id, 1, 9, 0),
24276 indent_guide(buffer_id, 6, 6, 1),
24277 indent_guide(buffer_id, 8, 8, 1),
24278 ],
24279 None,
24280 &mut cx,
24281 );
24282}
24283
24284#[gpui::test]
24285async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
24286 let (buffer_id, mut cx) = setup_indent_guides_editor(
24287 &"
24288 fn main() {
24289 if a {
24290 b(
24291 c,
24292 d,
24293 )
24294 } else {
24295 e(
24296 f
24297 )
24298 }
24299 }"
24300 .unindent(),
24301 cx,
24302 )
24303 .await;
24304
24305 assert_indent_guides(
24306 0..11,
24307 vec![
24308 indent_guide(buffer_id, 1, 10, 0),
24309 indent_guide(buffer_id, 2, 5, 1),
24310 indent_guide(buffer_id, 7, 9, 1),
24311 indent_guide(buffer_id, 3, 4, 2),
24312 indent_guide(buffer_id, 8, 8, 2),
24313 ],
24314 None,
24315 &mut cx,
24316 );
24317
24318 cx.update_editor(|editor, window, cx| {
24319 editor.fold_at(MultiBufferRow(2), window, cx);
24320 assert_eq!(
24321 editor.display_text(cx),
24322 "
24323 fn main() {
24324 if a {
24325 b(⋯)
24326 } else {
24327 e(
24328 f
24329 )
24330 }
24331 }"
24332 .unindent()
24333 );
24334 });
24335
24336 assert_indent_guides(
24337 0..11,
24338 vec![
24339 indent_guide(buffer_id, 1, 10, 0),
24340 indent_guide(buffer_id, 2, 5, 1),
24341 indent_guide(buffer_id, 7, 9, 1),
24342 indent_guide(buffer_id, 8, 8, 2),
24343 ],
24344 None,
24345 &mut cx,
24346 );
24347}
24348
24349#[gpui::test]
24350async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
24351 let (buffer_id, mut cx) = setup_indent_guides_editor(
24352 &"
24353 block1
24354 block2
24355 block3
24356 block4
24357 block2
24358 block1
24359 block1"
24360 .unindent(),
24361 cx,
24362 )
24363 .await;
24364
24365 assert_indent_guides(
24366 1..10,
24367 vec![
24368 indent_guide(buffer_id, 1, 4, 0),
24369 indent_guide(buffer_id, 2, 3, 1),
24370 indent_guide(buffer_id, 3, 3, 2),
24371 ],
24372 None,
24373 &mut cx,
24374 );
24375}
24376
24377#[gpui::test]
24378async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
24379 let (buffer_id, mut cx) = setup_indent_guides_editor(
24380 &"
24381 block1
24382 block2
24383 block3
24384
24385 block1
24386 block1"
24387 .unindent(),
24388 cx,
24389 )
24390 .await;
24391
24392 assert_indent_guides(
24393 0..6,
24394 vec![
24395 indent_guide(buffer_id, 1, 2, 0),
24396 indent_guide(buffer_id, 2, 2, 1),
24397 ],
24398 None,
24399 &mut cx,
24400 );
24401}
24402
24403#[gpui::test]
24404async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
24405 let (buffer_id, mut cx) = setup_indent_guides_editor(
24406 &"
24407 function component() {
24408 \treturn (
24409 \t\t\t
24410 \t\t<div>
24411 \t\t\t<abc></abc>
24412 \t\t</div>
24413 \t)
24414 }"
24415 .unindent(),
24416 cx,
24417 )
24418 .await;
24419
24420 assert_indent_guides(
24421 0..8,
24422 vec![
24423 indent_guide(buffer_id, 1, 6, 0),
24424 indent_guide(buffer_id, 2, 5, 1),
24425 indent_guide(buffer_id, 4, 4, 2),
24426 ],
24427 None,
24428 &mut cx,
24429 );
24430}
24431
24432#[gpui::test]
24433async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
24434 let (buffer_id, mut cx) = setup_indent_guides_editor(
24435 &"
24436 function component() {
24437 \treturn (
24438 \t
24439 \t\t<div>
24440 \t\t\t<abc></abc>
24441 \t\t</div>
24442 \t)
24443 }"
24444 .unindent(),
24445 cx,
24446 )
24447 .await;
24448
24449 assert_indent_guides(
24450 0..8,
24451 vec![
24452 indent_guide(buffer_id, 1, 6, 0),
24453 indent_guide(buffer_id, 2, 5, 1),
24454 indent_guide(buffer_id, 4, 4, 2),
24455 ],
24456 None,
24457 &mut cx,
24458 );
24459}
24460
24461#[gpui::test]
24462async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
24463 let (buffer_id, mut cx) = setup_indent_guides_editor(
24464 &"
24465 block1
24466
24467
24468
24469 block2
24470 "
24471 .unindent(),
24472 cx,
24473 )
24474 .await;
24475
24476 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
24477}
24478
24479#[gpui::test]
24480async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
24481 let (buffer_id, mut cx) = setup_indent_guides_editor(
24482 &"
24483 def a:
24484 \tb = 3
24485 \tif True:
24486 \t\tc = 4
24487 \t\td = 5
24488 \tprint(b)
24489 "
24490 .unindent(),
24491 cx,
24492 )
24493 .await;
24494
24495 assert_indent_guides(
24496 0..6,
24497 vec![
24498 indent_guide(buffer_id, 1, 5, 0),
24499 indent_guide(buffer_id, 3, 4, 1),
24500 ],
24501 None,
24502 &mut cx,
24503 );
24504}
24505
24506#[gpui::test]
24507async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
24508 let (buffer_id, mut cx) = setup_indent_guides_editor(
24509 &"
24510 fn main() {
24511 let a = 1;
24512 }"
24513 .unindent(),
24514 cx,
24515 )
24516 .await;
24517
24518 cx.update_editor(|editor, window, cx| {
24519 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24520 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
24521 });
24522 });
24523
24524 assert_indent_guides(
24525 0..3,
24526 vec![indent_guide(buffer_id, 1, 1, 0)],
24527 Some(vec![0]),
24528 &mut cx,
24529 );
24530}
24531
24532#[gpui::test]
24533async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
24534 let (buffer_id, mut cx) = setup_indent_guides_editor(
24535 &"
24536 fn main() {
24537 if 1 == 2 {
24538 let a = 1;
24539 }
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 cx.run_until_parked();
24552
24553 assert_indent_guides(
24554 0..4,
24555 vec![
24556 indent_guide(buffer_id, 1, 3, 0),
24557 indent_guide(buffer_id, 2, 2, 1),
24558 ],
24559 Some(vec![1]),
24560 &mut cx,
24561 );
24562
24563 cx.update_editor(|editor, window, cx| {
24564 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24565 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
24566 });
24567 });
24568 cx.run_until_parked();
24569
24570 assert_indent_guides(
24571 0..4,
24572 vec![
24573 indent_guide(buffer_id, 1, 3, 0),
24574 indent_guide(buffer_id, 2, 2, 1),
24575 ],
24576 Some(vec![1]),
24577 &mut cx,
24578 );
24579
24580 cx.update_editor(|editor, window, cx| {
24581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24582 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
24583 });
24584 });
24585 cx.run_until_parked();
24586
24587 assert_indent_guides(
24588 0..4,
24589 vec![
24590 indent_guide(buffer_id, 1, 3, 0),
24591 indent_guide(buffer_id, 2, 2, 1),
24592 ],
24593 Some(vec![0]),
24594 &mut cx,
24595 );
24596}
24597
24598#[gpui::test]
24599async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
24600 let (buffer_id, mut cx) = setup_indent_guides_editor(
24601 &"
24602 fn main() {
24603 let a = 1;
24604
24605 let b = 2;
24606 }"
24607 .unindent(),
24608 cx,
24609 )
24610 .await;
24611
24612 cx.update_editor(|editor, window, cx| {
24613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24614 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
24615 });
24616 });
24617
24618 assert_indent_guides(
24619 0..5,
24620 vec![indent_guide(buffer_id, 1, 3, 0)],
24621 Some(vec![0]),
24622 &mut cx,
24623 );
24624}
24625
24626#[gpui::test]
24627async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
24628 let (buffer_id, mut cx) = setup_indent_guides_editor(
24629 &"
24630 def m:
24631 a = 1
24632 pass"
24633 .unindent(),
24634 cx,
24635 )
24636 .await;
24637
24638 cx.update_editor(|editor, window, cx| {
24639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24640 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
24641 });
24642 });
24643
24644 assert_indent_guides(
24645 0..3,
24646 vec![indent_guide(buffer_id, 1, 2, 0)],
24647 Some(vec![0]),
24648 &mut cx,
24649 );
24650}
24651
24652#[gpui::test]
24653async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
24654 init_test(cx, |_| {});
24655 let mut cx = EditorTestContext::new(cx).await;
24656 let text = indoc! {
24657 "
24658 impl A {
24659 fn b() {
24660 0;
24661 3;
24662 5;
24663 6;
24664 7;
24665 }
24666 }
24667 "
24668 };
24669 let base_text = indoc! {
24670 "
24671 impl A {
24672 fn b() {
24673 0;
24674 1;
24675 2;
24676 3;
24677 4;
24678 }
24679 fn c() {
24680 5;
24681 6;
24682 7;
24683 }
24684 }
24685 "
24686 };
24687
24688 cx.update_editor(|editor, window, cx| {
24689 editor.set_text(text, window, cx);
24690
24691 editor.buffer().update(cx, |multibuffer, cx| {
24692 let buffer = multibuffer.as_singleton().unwrap();
24693 let diff = cx.new(|cx| {
24694 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
24695 });
24696
24697 multibuffer.set_all_diff_hunks_expanded(cx);
24698 multibuffer.add_diff(diff, cx);
24699
24700 buffer.read(cx).remote_id()
24701 })
24702 });
24703 cx.run_until_parked();
24704
24705 cx.assert_state_with_diff(
24706 indoc! { "
24707 impl A {
24708 fn b() {
24709 0;
24710 - 1;
24711 - 2;
24712 3;
24713 - 4;
24714 - }
24715 - fn c() {
24716 5;
24717 6;
24718 7;
24719 }
24720 }
24721 ˇ"
24722 }
24723 .to_string(),
24724 );
24725
24726 let mut actual_guides = cx.update_editor(|editor, window, cx| {
24727 editor
24728 .snapshot(window, cx)
24729 .buffer_snapshot()
24730 .indent_guides_in_range(Anchor::Min..Anchor::Max, false, cx)
24731 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
24732 .collect::<Vec<_>>()
24733 });
24734 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
24735 assert_eq!(
24736 actual_guides,
24737 vec![
24738 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
24739 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
24740 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
24741 ]
24742 );
24743}
24744
24745#[gpui::test]
24746async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
24747 init_test(cx, |_| {});
24748 let mut cx = EditorTestContext::new(cx).await;
24749
24750 let diff_base = r#"
24751 a
24752 b
24753 c
24754 "#
24755 .unindent();
24756
24757 cx.set_state(
24758 &r#"
24759 ˇA
24760 b
24761 C
24762 "#
24763 .unindent(),
24764 );
24765 cx.set_head_text(&diff_base);
24766 cx.update_editor(|editor, window, cx| {
24767 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24768 });
24769 executor.run_until_parked();
24770
24771 let both_hunks_expanded = r#"
24772 - a
24773 + ˇA
24774 b
24775 - c
24776 + C
24777 "#
24778 .unindent();
24779
24780 cx.assert_state_with_diff(both_hunks_expanded.clone());
24781
24782 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24783 let snapshot = editor.snapshot(window, cx);
24784 let hunks = editor
24785 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24786 .collect::<Vec<_>>();
24787 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
24788 hunks
24789 .into_iter()
24790 .map(|hunk| {
24791 multibuffer_snapshot
24792 .anchor_in_excerpt(hunk.buffer_range.start)
24793 .unwrap()
24794 ..multibuffer_snapshot
24795 .anchor_in_excerpt(hunk.buffer_range.end)
24796 .unwrap()
24797 })
24798 .collect::<Vec<_>>()
24799 });
24800 assert_eq!(hunk_ranges.len(), 2);
24801
24802 cx.update_editor(|editor, _, cx| {
24803 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24804 });
24805 executor.run_until_parked();
24806
24807 let second_hunk_expanded = r#"
24808 ˇA
24809 b
24810 - c
24811 + C
24812 "#
24813 .unindent();
24814
24815 cx.assert_state_with_diff(second_hunk_expanded);
24816
24817 cx.update_editor(|editor, _, cx| {
24818 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24819 });
24820 executor.run_until_parked();
24821
24822 cx.assert_state_with_diff(both_hunks_expanded.clone());
24823
24824 cx.update_editor(|editor, _, cx| {
24825 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24826 });
24827 executor.run_until_parked();
24828
24829 let first_hunk_expanded = r#"
24830 - a
24831 + ˇA
24832 b
24833 C
24834 "#
24835 .unindent();
24836
24837 cx.assert_state_with_diff(first_hunk_expanded);
24838
24839 cx.update_editor(|editor, _, cx| {
24840 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24841 });
24842 executor.run_until_parked();
24843
24844 cx.assert_state_with_diff(both_hunks_expanded);
24845
24846 cx.set_state(
24847 &r#"
24848 ˇA
24849 b
24850 "#
24851 .unindent(),
24852 );
24853 cx.run_until_parked();
24854
24855 // TODO this cursor position seems bad
24856 cx.assert_state_with_diff(
24857 r#"
24858 - ˇa
24859 + A
24860 b
24861 "#
24862 .unindent(),
24863 );
24864
24865 cx.update_editor(|editor, window, cx| {
24866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24867 });
24868
24869 cx.assert_state_with_diff(
24870 r#"
24871 - ˇa
24872 + A
24873 b
24874 - c
24875 "#
24876 .unindent(),
24877 );
24878
24879 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24880 let snapshot = editor.snapshot(window, cx);
24881 let hunks = editor
24882 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24883 .collect::<Vec<_>>();
24884 let multibuffer_snapshot = snapshot.buffer_snapshot();
24885 hunks
24886 .into_iter()
24887 .map(|hunk| {
24888 multibuffer_snapshot
24889 .anchor_in_excerpt(hunk.buffer_range.start)
24890 .unwrap()
24891 ..multibuffer_snapshot
24892 .anchor_in_excerpt(hunk.buffer_range.end)
24893 .unwrap()
24894 })
24895 .collect::<Vec<_>>()
24896 });
24897 assert_eq!(hunk_ranges.len(), 2);
24898
24899 cx.update_editor(|editor, _, cx| {
24900 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
24901 });
24902 executor.run_until_parked();
24903
24904 cx.assert_state_with_diff(
24905 r#"
24906 - ˇa
24907 + A
24908 b
24909 "#
24910 .unindent(),
24911 );
24912}
24913
24914#[gpui::test]
24915async fn test_toggle_deletion_hunk_at_start_of_file(
24916 executor: BackgroundExecutor,
24917 cx: &mut TestAppContext,
24918) {
24919 init_test(cx, |_| {});
24920 let mut cx = EditorTestContext::new(cx).await;
24921
24922 let diff_base = r#"
24923 a
24924 b
24925 c
24926 "#
24927 .unindent();
24928
24929 cx.set_state(
24930 &r#"
24931 ˇb
24932 c
24933 "#
24934 .unindent(),
24935 );
24936 cx.set_head_text(&diff_base);
24937 cx.update_editor(|editor, window, cx| {
24938 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24939 });
24940 executor.run_until_parked();
24941
24942 let hunk_expanded = r#"
24943 - a
24944 ˇb
24945 c
24946 "#
24947 .unindent();
24948
24949 cx.assert_state_with_diff(hunk_expanded.clone());
24950
24951 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24952 let snapshot = editor.snapshot(window, cx);
24953 let hunks = editor
24954 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
24955 .collect::<Vec<_>>();
24956 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
24957 hunks
24958 .into_iter()
24959 .map(|hunk| {
24960 multibuffer_snapshot
24961 .anchor_in_excerpt(hunk.buffer_range.start)
24962 .unwrap()
24963 ..multibuffer_snapshot
24964 .anchor_in_excerpt(hunk.buffer_range.end)
24965 .unwrap()
24966 })
24967 .collect::<Vec<_>>()
24968 });
24969 assert_eq!(hunk_ranges.len(), 1);
24970
24971 cx.update_editor(|editor, _, cx| {
24972 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24973 });
24974 executor.run_until_parked();
24975
24976 let hunk_collapsed = r#"
24977 ˇb
24978 c
24979 "#
24980 .unindent();
24981
24982 cx.assert_state_with_diff(hunk_collapsed);
24983
24984 cx.update_editor(|editor, _, cx| {
24985 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24986 });
24987 executor.run_until_parked();
24988
24989 cx.assert_state_with_diff(hunk_expanded);
24990}
24991
24992#[gpui::test]
24993async fn test_select_smaller_syntax_node_after_diff_hunk_collapse(
24994 executor: BackgroundExecutor,
24995 cx: &mut TestAppContext,
24996) {
24997 init_test(cx, |_| {});
24998
24999 let mut cx = EditorTestContext::new(cx).await;
25000 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
25001
25002 cx.set_state(
25003 &r#"
25004 fn main() {
25005 let x = ˇ1;
25006 }
25007 "#
25008 .unindent(),
25009 );
25010
25011 let diff_base = r#"
25012 fn removed_one() {
25013 println!("this function was deleted");
25014 }
25015
25016 fn removed_two() {
25017 println!("this function was also deleted");
25018 }
25019
25020 fn main() {
25021 let x = 1;
25022 }
25023 "#
25024 .unindent();
25025 cx.set_head_text(&diff_base);
25026 executor.run_until_parked();
25027
25028 cx.update_editor(|editor, window, cx| {
25029 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
25030 });
25031 executor.run_until_parked();
25032
25033 cx.update_editor(|editor, window, cx| {
25034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
25035 });
25036
25037 cx.update_editor(|editor, window, cx| {
25038 editor.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
25039 });
25040 executor.run_until_parked();
25041
25042 cx.update_editor(|editor, window, cx| {
25043 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
25044 });
25045}
25046
25047#[gpui::test]
25048async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
25049 executor: BackgroundExecutor,
25050 cx: &mut TestAppContext,
25051) {
25052 init_test(cx, |_| {});
25053 let mut cx = EditorTestContext::new(cx).await;
25054
25055 cx.set_state("ˇnew\nsecond\nthird\n");
25056 cx.set_head_text("old\nsecond\nthird\n");
25057 cx.update_editor(|editor, window, cx| {
25058 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
25059 });
25060 executor.run_until_parked();
25061 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
25062
25063 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
25064 cx.update_editor(|editor, window, cx| {
25065 let snapshot = editor.snapshot(window, cx);
25066 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
25067 let hunks = editor
25068 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
25069 .collect::<Vec<_>>();
25070 assert_eq!(hunks.len(), 1);
25071 let hunk_range = multibuffer_snapshot
25072 .anchor_in_excerpt(hunks[0].buffer_range.start)
25073 .unwrap()
25074 ..multibuffer_snapshot
25075 .anchor_in_excerpt(hunks[0].buffer_range.end)
25076 .unwrap();
25077 editor.toggle_single_diff_hunk(hunk_range, cx)
25078 });
25079 executor.run_until_parked();
25080 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
25081
25082 // Keep the editor scrolled to the top so the full hunk remains visible.
25083 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
25084}
25085
25086#[gpui::test]
25087async fn test_display_diff_hunks(cx: &mut TestAppContext) {
25088 init_test(cx, |_| {});
25089
25090 let fs = FakeFs::new(cx.executor());
25091 fs.insert_tree(
25092 path!("/test"),
25093 json!({
25094 ".git": {},
25095 "file-1": "ONE\n",
25096 "file-2": "TWO\n",
25097 "file-3": "THREE\n",
25098 }),
25099 )
25100 .await;
25101
25102 fs.set_head_for_repo(
25103 path!("/test/.git").as_ref(),
25104 &[
25105 ("file-1", "one\n".into()),
25106 ("file-2", "two\n".into()),
25107 ("file-3", "three\n".into()),
25108 ],
25109 "deadbeef",
25110 );
25111
25112 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
25113 let mut buffers = vec![];
25114 for i in 1..=3 {
25115 let buffer = project
25116 .update(cx, |project, cx| {
25117 let path = format!(path!("/test/file-{}"), i);
25118 project.open_local_buffer(path, cx)
25119 })
25120 .await
25121 .unwrap();
25122 buffers.push(buffer);
25123 }
25124
25125 let multibuffer = cx.new(|cx| {
25126 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
25127 multibuffer.set_all_diff_hunks_expanded(cx);
25128 for buffer in &buffers {
25129 let snapshot = buffer.read(cx).snapshot();
25130 multibuffer.set_excerpts_for_path(
25131 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
25132 buffer.clone(),
25133 vec![Point::zero()..snapshot.max_point()],
25134 2,
25135 cx,
25136 );
25137 }
25138 multibuffer
25139 });
25140
25141 let editor = cx.add_window(|window, cx| {
25142 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
25143 });
25144 cx.run_until_parked();
25145
25146 let snapshot = editor
25147 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25148 .unwrap();
25149 let hunks = snapshot
25150 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
25151 .map(|hunk| match hunk {
25152 DisplayDiffHunk::Unfolded {
25153 display_row_range, ..
25154 } => display_row_range,
25155 DisplayDiffHunk::Folded { .. } => unreachable!(),
25156 })
25157 .collect::<Vec<_>>();
25158 assert_eq!(
25159 hunks,
25160 [
25161 DisplayRow(2)..DisplayRow(4),
25162 DisplayRow(7)..DisplayRow(9),
25163 DisplayRow(12)..DisplayRow(14),
25164 ]
25165 );
25166}
25167
25168#[gpui::test]
25169async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
25170 init_test(cx, |_| {});
25171
25172 let mut cx = EditorTestContext::new(cx).await;
25173 cx.set_head_text(indoc! { "
25174 one
25175 two
25176 three
25177 four
25178 five
25179 "
25180 });
25181 cx.set_index_text(indoc! { "
25182 one
25183 two
25184 three
25185 four
25186 five
25187 "
25188 });
25189 cx.set_state(indoc! {"
25190 one
25191 TWO
25192 ˇTHREE
25193 FOUR
25194 five
25195 "});
25196 cx.run_until_parked();
25197 cx.update_editor(|editor, window, cx| {
25198 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
25199 });
25200 cx.run_until_parked();
25201 cx.assert_index_text(Some(indoc! {"
25202 one
25203 TWO
25204 THREE
25205 FOUR
25206 five
25207 "}));
25208 cx.set_state(indoc! { "
25209 one
25210 TWO
25211 ˇTHREE-HUNDRED
25212 FOUR
25213 five
25214 "});
25215 cx.run_until_parked();
25216 cx.update_editor(|editor, window, cx| {
25217 let snapshot = editor.snapshot(window, cx);
25218 let hunks = editor
25219 .diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], &snapshot.buffer_snapshot())
25220 .collect::<Vec<_>>();
25221 assert_eq!(hunks.len(), 1);
25222 assert_eq!(
25223 hunks[0].status(),
25224 DiffHunkStatus {
25225 kind: DiffHunkStatusKind::Modified,
25226 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
25227 }
25228 );
25229
25230 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
25231 });
25232 cx.run_until_parked();
25233 cx.assert_index_text(Some(indoc! {"
25234 one
25235 TWO
25236 THREE-HUNDRED
25237 FOUR
25238 five
25239 "}));
25240}
25241
25242#[gpui::test]
25243fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
25244 init_test(cx, |_| {});
25245
25246 let editor = cx.add_window(|window, cx| {
25247 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
25248 build_editor(buffer, window, cx)
25249 });
25250
25251 let render_args = Arc::new(Mutex::new(None));
25252 let snapshot = editor
25253 .update(cx, |editor, window, cx| {
25254 let snapshot = editor.buffer().read(cx).snapshot(cx);
25255 let range =
25256 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
25257
25258 struct RenderArgs {
25259 row: MultiBufferRow,
25260 folded: bool,
25261 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
25262 }
25263
25264 let crease = Crease::inline(
25265 range,
25266 FoldPlaceholder::test(),
25267 {
25268 let toggle_callback = render_args.clone();
25269 move |row, folded, callback, _window, _cx| {
25270 *toggle_callback.lock() = Some(RenderArgs {
25271 row,
25272 folded,
25273 callback,
25274 });
25275 div()
25276 }
25277 },
25278 |_row, _folded, _window, _cx| div(),
25279 );
25280
25281 editor.insert_creases(Some(crease), cx);
25282 let snapshot = editor.snapshot(window, cx);
25283 let _div =
25284 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
25285 snapshot
25286 })
25287 .unwrap();
25288
25289 let render_args = render_args.lock().take().unwrap();
25290 assert_eq!(render_args.row, MultiBufferRow(1));
25291 assert!(!render_args.folded);
25292 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
25293
25294 cx.update_window(*editor, |_, window, cx| {
25295 (render_args.callback)(true, window, cx)
25296 })
25297 .unwrap();
25298 let snapshot = editor
25299 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25300 .unwrap();
25301 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
25302
25303 cx.update_window(*editor, |_, window, cx| {
25304 (render_args.callback)(false, window, cx)
25305 })
25306 .unwrap();
25307 let snapshot = editor
25308 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
25309 .unwrap();
25310 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
25311}
25312
25313#[gpui::test]
25314async fn test_input_text(cx: &mut TestAppContext) {
25315 init_test(cx, |_| {});
25316 let mut cx = EditorTestContext::new(cx).await;
25317
25318 cx.set_state(
25319 &r#"ˇone
25320 two
25321
25322 three
25323 fourˇ
25324 five
25325
25326 siˇx"#
25327 .unindent(),
25328 );
25329
25330 cx.dispatch_action(HandleInput(String::new()));
25331 cx.assert_editor_state(
25332 &r#"ˇone
25333 two
25334
25335 three
25336 fourˇ
25337 five
25338
25339 siˇx"#
25340 .unindent(),
25341 );
25342
25343 cx.dispatch_action(HandleInput("AAAA".to_string()));
25344 cx.assert_editor_state(
25345 &r#"AAAAˇone
25346 two
25347
25348 three
25349 fourAAAAˇ
25350 five
25351
25352 siAAAAˇx"#
25353 .unindent(),
25354 );
25355}
25356
25357#[gpui::test]
25358async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
25359 init_test(cx, |_| {});
25360
25361 let mut cx = EditorTestContext::new(cx).await;
25362 cx.set_state(
25363 r#"let foo = 1;
25364let foo = 2;
25365let foo = 3;
25366let fooˇ = 4;
25367let foo = 5;
25368let foo = 6;
25369let foo = 7;
25370let foo = 8;
25371let foo = 9;
25372let foo = 10;
25373let foo = 11;
25374let foo = 12;
25375let foo = 13;
25376let foo = 14;
25377let foo = 15;"#,
25378 );
25379
25380 cx.update_editor(|e, window, cx| {
25381 assert_eq!(
25382 e.next_scroll_position,
25383 NextScrollCursorCenterTopBottom::Center,
25384 "Default next scroll direction is center",
25385 );
25386
25387 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25388 assert_eq!(
25389 e.next_scroll_position,
25390 NextScrollCursorCenterTopBottom::Top,
25391 "After center, next scroll direction should be top",
25392 );
25393
25394 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25395 assert_eq!(
25396 e.next_scroll_position,
25397 NextScrollCursorCenterTopBottom::Bottom,
25398 "After top, next scroll direction should be bottom",
25399 );
25400
25401 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25402 assert_eq!(
25403 e.next_scroll_position,
25404 NextScrollCursorCenterTopBottom::Center,
25405 "After bottom, scrolling should start over",
25406 );
25407
25408 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
25409 assert_eq!(
25410 e.next_scroll_position,
25411 NextScrollCursorCenterTopBottom::Top,
25412 "Scrolling continues if retriggered fast enough"
25413 );
25414 });
25415
25416 cx.executor()
25417 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
25418 cx.executor().run_until_parked();
25419 cx.update_editor(|e, _, _| {
25420 assert_eq!(
25421 e.next_scroll_position,
25422 NextScrollCursorCenterTopBottom::Center,
25423 "If scrolling is not triggered fast enough, it should reset"
25424 );
25425 });
25426}
25427
25428#[gpui::test]
25429async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
25430 init_test(cx, |_| {});
25431 let mut cx = EditorLspTestContext::new_rust(
25432 lsp::ServerCapabilities {
25433 definition_provider: Some(lsp::OneOf::Left(true)),
25434 references_provider: Some(lsp::OneOf::Left(true)),
25435 ..lsp::ServerCapabilities::default()
25436 },
25437 cx,
25438 )
25439 .await;
25440
25441 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
25442 let go_to_definition = cx
25443 .lsp
25444 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
25445 move |params, _| async move {
25446 if empty_go_to_definition {
25447 Ok(None)
25448 } else {
25449 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
25450 uri: params.text_document_position_params.text_document.uri,
25451 range: lsp::Range::new(
25452 lsp::Position::new(4, 3),
25453 lsp::Position::new(4, 6),
25454 ),
25455 })))
25456 }
25457 },
25458 );
25459 let references = cx
25460 .lsp
25461 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25462 Ok(Some(vec![lsp::Location {
25463 uri: params.text_document_position.text_document.uri,
25464 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
25465 }]))
25466 });
25467 (go_to_definition, references)
25468 };
25469
25470 cx.set_state(
25471 &r#"fn one() {
25472 let mut a = ˇtwo();
25473 }
25474
25475 fn two() {}"#
25476 .unindent(),
25477 );
25478 set_up_lsp_handlers(false, &mut cx);
25479 let navigated = cx
25480 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25481 .await
25482 .expect("Failed to navigate to definition");
25483 assert_eq!(
25484 navigated,
25485 Navigated::Yes,
25486 "Should have navigated to definition from the GetDefinition response"
25487 );
25488 cx.assert_editor_state(
25489 &r#"fn one() {
25490 let mut a = two();
25491 }
25492
25493 fn «twoˇ»() {}"#
25494 .unindent(),
25495 );
25496
25497 let editors = cx.update_workspace(|workspace, _, cx| {
25498 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25499 });
25500 cx.update_editor(|_, _, test_editor_cx| {
25501 assert_eq!(
25502 editors.len(),
25503 1,
25504 "Initially, only one, test, editor should be open in the workspace"
25505 );
25506 assert_eq!(
25507 test_editor_cx.entity(),
25508 editors.last().expect("Asserted len is 1").clone()
25509 );
25510 });
25511
25512 set_up_lsp_handlers(true, &mut cx);
25513 let navigated = cx
25514 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25515 .await
25516 .expect("Failed to navigate to lookup references");
25517 assert_eq!(
25518 navigated,
25519 Navigated::Yes,
25520 "Should have navigated to references as a fallback after empty GoToDefinition response"
25521 );
25522 // We should not change the selections in the existing file,
25523 // if opening another milti buffer with the references
25524 cx.assert_editor_state(
25525 &r#"fn one() {
25526 let mut a = two();
25527 }
25528
25529 fn «twoˇ»() {}"#
25530 .unindent(),
25531 );
25532 let editors = cx.update_workspace(|workspace, _, cx| {
25533 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25534 });
25535 cx.update_editor(|_, _, test_editor_cx| {
25536 assert_eq!(
25537 editors.len(),
25538 2,
25539 "After falling back to references search, we open a new editor with the results"
25540 );
25541 let references_fallback_text = editors
25542 .into_iter()
25543 .find(|new_editor| *new_editor != test_editor_cx.entity())
25544 .expect("Should have one non-test editor now")
25545 .read(test_editor_cx)
25546 .text(test_editor_cx);
25547 assert_eq!(
25548 references_fallback_text, "fn one() {\n let mut a = two();\n}",
25549 "Should use the range from the references response and not the GoToDefinition one"
25550 );
25551 });
25552}
25553
25554#[gpui::test]
25555async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
25556 init_test(cx, |_| {});
25557 cx.update(|cx| {
25558 let mut editor_settings = EditorSettings::get_global(cx).clone();
25559 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
25560 EditorSettings::override_global(editor_settings, cx);
25561 });
25562 let mut cx = EditorLspTestContext::new_rust(
25563 lsp::ServerCapabilities {
25564 definition_provider: Some(lsp::OneOf::Left(true)),
25565 references_provider: Some(lsp::OneOf::Left(true)),
25566 ..lsp::ServerCapabilities::default()
25567 },
25568 cx,
25569 )
25570 .await;
25571 let original_state = r#"fn one() {
25572 let mut a = ˇtwo();
25573 }
25574
25575 fn two() {}"#
25576 .unindent();
25577 cx.set_state(&original_state);
25578
25579 let mut go_to_definition = cx
25580 .lsp
25581 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
25582 move |_, _| async move { Ok(None) },
25583 );
25584 let _references = cx
25585 .lsp
25586 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
25587 panic!("Should not call for references with no go to definition fallback")
25588 });
25589
25590 let navigated = cx
25591 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25592 .await
25593 .expect("Failed to navigate to lookup references");
25594 go_to_definition
25595 .next()
25596 .await
25597 .expect("Should have called the go_to_definition handler");
25598
25599 assert_eq!(
25600 navigated,
25601 Navigated::No,
25602 "Should have navigated to references as a fallback after empty GoToDefinition response"
25603 );
25604 cx.assert_editor_state(&original_state);
25605 let editors = cx.update_workspace(|workspace, _, cx| {
25606 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25607 });
25608 cx.update_editor(|_, _, _| {
25609 assert_eq!(
25610 editors.len(),
25611 1,
25612 "After unsuccessful fallback, no other editor should have been opened"
25613 );
25614 });
25615}
25616
25617#[gpui::test]
25618async fn test_goto_definition_close_ranges_open_singleton(cx: &mut TestAppContext) {
25619 init_test(cx, |_| {});
25620 let mut cx = EditorLspTestContext::new_rust(
25621 lsp::ServerCapabilities {
25622 definition_provider: Some(lsp::OneOf::Left(true)),
25623 ..lsp::ServerCapabilities::default()
25624 },
25625 cx,
25626 )
25627 .await;
25628
25629 // File content: 10 lines with functions defined on lines 3, 5, and 7 (0-indexed).
25630 // With the default excerpt_context_lines of 2, ranges that are within
25631 // 2 * 2 = 4 rows of each other should be grouped into one excerpt.
25632 cx.set_state(
25633 &r#"fn caller() {
25634 let _ = ˇtarget();
25635 }
25636 fn target_a() {}
25637
25638 fn target_b() {}
25639
25640 fn target_c() {}
25641 "#
25642 .unindent(),
25643 );
25644
25645 // Return two definitions that are close together (lines 3 and 5, gap of 2 rows)
25646 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25647 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25648 lsp::Location {
25649 uri: url.clone(),
25650 range: lsp::Range::new(lsp::Position::new(3, 3), lsp::Position::new(3, 11)),
25651 },
25652 lsp::Location {
25653 uri: url,
25654 range: lsp::Range::new(lsp::Position::new(5, 3), lsp::Position::new(5, 11)),
25655 },
25656 ])))
25657 });
25658
25659 let navigated = cx
25660 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25661 .await
25662 .expect("Failed to navigate to definitions");
25663 assert_eq!(navigated, Navigated::Yes);
25664
25665 let editors = cx.update_workspace(|workspace, _, cx| {
25666 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25667 });
25668 cx.update_editor(|_, _, _| {
25669 assert_eq!(
25670 editors.len(),
25671 1,
25672 "Close ranges should navigate in-place without opening a new editor"
25673 );
25674 });
25675
25676 // Both target ranges should be selected
25677 cx.assert_editor_state(
25678 &r#"fn caller() {
25679 let _ = target();
25680 }
25681 fn «target_aˇ»() {}
25682
25683 fn «target_bˇ»() {}
25684
25685 fn target_c() {}
25686 "#
25687 .unindent(),
25688 );
25689}
25690
25691#[gpui::test]
25692async fn test_goto_definition_far_ranges_open_multibuffer(cx: &mut TestAppContext) {
25693 init_test(cx, |_| {});
25694 let mut cx = EditorLspTestContext::new_rust(
25695 lsp::ServerCapabilities {
25696 definition_provider: Some(lsp::OneOf::Left(true)),
25697 ..lsp::ServerCapabilities::default()
25698 },
25699 cx,
25700 )
25701 .await;
25702
25703 // Create a file with definitions far apart (more than 2 * excerpt_context_lines rows).
25704 cx.set_state(
25705 &r#"fn caller() {
25706 let _ = ˇtarget();
25707 }
25708 fn target_a() {}
25709
25710
25711
25712
25713
25714
25715
25716
25717
25718
25719
25720
25721
25722
25723
25724 fn target_b() {}
25725 "#
25726 .unindent(),
25727 );
25728
25729 // Return two definitions that are far apart (lines 3 and 19, gap of 16 rows)
25730 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25731 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25732 lsp::Location {
25733 uri: url.clone(),
25734 range: lsp::Range::new(lsp::Position::new(3, 3), lsp::Position::new(3, 11)),
25735 },
25736 lsp::Location {
25737 uri: url,
25738 range: lsp::Range::new(lsp::Position::new(19, 3), lsp::Position::new(19, 11)),
25739 },
25740 ])))
25741 });
25742
25743 let navigated = cx
25744 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25745 .await
25746 .expect("Failed to navigate to definitions");
25747 assert_eq!(navigated, Navigated::Yes);
25748
25749 let editors = cx.update_workspace(|workspace, _, cx| {
25750 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25751 });
25752 cx.update_editor(|_, _, test_editor_cx| {
25753 assert_eq!(
25754 editors.len(),
25755 2,
25756 "Far apart ranges should open a new multibuffer editor"
25757 );
25758 let multibuffer_editor = editors
25759 .into_iter()
25760 .find(|editor| *editor != test_editor_cx.entity())
25761 .expect("Should have a multibuffer editor");
25762 let multibuffer_text = multibuffer_editor.read(test_editor_cx).text(test_editor_cx);
25763 assert!(
25764 multibuffer_text.contains("target_a"),
25765 "Multibuffer should contain the first definition"
25766 );
25767 assert!(
25768 multibuffer_text.contains("target_b"),
25769 "Multibuffer should contain the second definition"
25770 );
25771 });
25772}
25773
25774#[gpui::test]
25775async fn test_goto_definition_contained_ranges(cx: &mut TestAppContext) {
25776 init_test(cx, |_| {});
25777 let mut cx = EditorLspTestContext::new_rust(
25778 lsp::ServerCapabilities {
25779 definition_provider: Some(lsp::OneOf::Left(true)),
25780 ..lsp::ServerCapabilities::default()
25781 },
25782 cx,
25783 )
25784 .await;
25785
25786 // The LSP returns two single-line definitions on the same row where one
25787 // range contains the other. Both are on the same line so the
25788 // `fits_in_one_excerpt` check won't underflow, and the code reaches
25789 // `change_selections`.
25790 cx.set_state(
25791 &r#"fn caller() {
25792 let _ = ˇtarget();
25793 }
25794 fn target_outer() { fn target_inner() {} }
25795 "#
25796 .unindent(),
25797 );
25798
25799 // Return two definitions on the same line: an outer range covering the
25800 // whole line and an inner range for just the inner function name.
25801 cx.set_request_handler::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
25802 Ok(Some(lsp::GotoDefinitionResponse::Array(vec![
25803 // Inner range: just "target_inner" (cols 23..35)
25804 lsp::Location {
25805 uri: url.clone(),
25806 range: lsp::Range::new(lsp::Position::new(3, 23), lsp::Position::new(3, 35)),
25807 },
25808 // Outer range: the whole line (cols 0..48)
25809 lsp::Location {
25810 uri: url,
25811 range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 48)),
25812 },
25813 ])))
25814 });
25815
25816 let navigated = cx
25817 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
25818 .await
25819 .expect("Failed to navigate to definitions");
25820 assert_eq!(navigated, Navigated::Yes);
25821}
25822
25823#[gpui::test]
25824async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
25825 init_test(cx, |_| {});
25826 let mut cx = EditorLspTestContext::new_rust(
25827 lsp::ServerCapabilities {
25828 references_provider: Some(lsp::OneOf::Left(true)),
25829 ..lsp::ServerCapabilities::default()
25830 },
25831 cx,
25832 )
25833 .await;
25834
25835 cx.set_state(
25836 &r#"
25837 fn one() {
25838 let mut a = two();
25839 }
25840
25841 fn ˇtwo() {}"#
25842 .unindent(),
25843 );
25844 cx.lsp
25845 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25846 Ok(Some(vec![
25847 lsp::Location {
25848 uri: params.text_document_position.text_document.uri.clone(),
25849 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
25850 },
25851 lsp::Location {
25852 uri: params.text_document_position.text_document.uri,
25853 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
25854 },
25855 ]))
25856 });
25857 let navigated = cx
25858 .update_editor(|editor, window, cx| {
25859 editor.find_all_references(&FindAllReferences::default(), window, cx)
25860 })
25861 .unwrap()
25862 .await
25863 .expect("Failed to navigate to references");
25864 assert_eq!(
25865 navigated,
25866 Navigated::Yes,
25867 "Should have navigated to references from the FindAllReferences response"
25868 );
25869 cx.assert_editor_state(
25870 &r#"fn one() {
25871 let mut a = two();
25872 }
25873
25874 fn ˇtwo() {}"#
25875 .unindent(),
25876 );
25877
25878 let editors = cx.update_workspace(|workspace, _, cx| {
25879 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25880 });
25881 cx.update_editor(|_, _, _| {
25882 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
25883 });
25884
25885 cx.set_state(
25886 &r#"fn one() {
25887 let mut a = ˇtwo();
25888 }
25889
25890 fn two() {}"#
25891 .unindent(),
25892 );
25893 let navigated = cx
25894 .update_editor(|editor, window, cx| {
25895 editor.find_all_references(&FindAllReferences::default(), window, cx)
25896 })
25897 .unwrap()
25898 .await
25899 .expect("Failed to navigate to references");
25900 assert_eq!(
25901 navigated,
25902 Navigated::Yes,
25903 "Should have navigated to references from the FindAllReferences response"
25904 );
25905 cx.assert_editor_state(
25906 &r#"fn one() {
25907 let mut a = ˇtwo();
25908 }
25909
25910 fn two() {}"#
25911 .unindent(),
25912 );
25913 let editors = cx.update_workspace(|workspace, _, cx| {
25914 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25915 });
25916 cx.update_editor(|_, _, _| {
25917 assert_eq!(
25918 editors.len(),
25919 2,
25920 "should have re-used the previous multibuffer"
25921 );
25922 });
25923
25924 cx.set_state(
25925 &r#"fn one() {
25926 let mut a = ˇtwo();
25927 }
25928 fn three() {}
25929 fn two() {}"#
25930 .unindent(),
25931 );
25932 cx.lsp
25933 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
25934 Ok(Some(vec![
25935 lsp::Location {
25936 uri: params.text_document_position.text_document.uri.clone(),
25937 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
25938 },
25939 lsp::Location {
25940 uri: params.text_document_position.text_document.uri,
25941 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
25942 },
25943 ]))
25944 });
25945 let navigated = cx
25946 .update_editor(|editor, window, cx| {
25947 editor.find_all_references(&FindAllReferences::default(), window, cx)
25948 })
25949 .unwrap()
25950 .await
25951 .expect("Failed to navigate to references");
25952 assert_eq!(
25953 navigated,
25954 Navigated::Yes,
25955 "Should have navigated to references from the FindAllReferences response"
25956 );
25957 cx.assert_editor_state(
25958 &r#"fn one() {
25959 let mut a = ˇtwo();
25960 }
25961 fn three() {}
25962 fn two() {}"#
25963 .unindent(),
25964 );
25965 let editors = cx.update_workspace(|workspace, _, cx| {
25966 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
25967 });
25968 cx.update_editor(|_, _, _| {
25969 assert_eq!(
25970 editors.len(),
25971 3,
25972 "should have used a new multibuffer as offsets changed"
25973 );
25974 });
25975}
25976#[gpui::test]
25977async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
25978 init_test(cx, |_| {});
25979
25980 let language = Arc::new(Language::new(
25981 LanguageConfig::default(),
25982 Some(tree_sitter_rust::LANGUAGE.into()),
25983 ));
25984
25985 let text = r#"
25986 #[cfg(test)]
25987 mod tests() {
25988 #[test]
25989 fn runnable_1() {
25990 let a = 1;
25991 }
25992
25993 #[test]
25994 fn runnable_2() {
25995 let a = 1;
25996 let b = 2;
25997 }
25998 }
25999 "#
26000 .unindent();
26001
26002 let fs = FakeFs::new(cx.executor());
26003 fs.insert_file("/file.rs", Default::default()).await;
26004
26005 let project = Project::test(fs, ["/a".as_ref()], cx).await;
26006 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26007 let cx = &mut VisualTestContext::from_window(*window, cx);
26008 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26009 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
26010
26011 let editor = cx.new_window_entity(|window, cx| {
26012 Editor::new(
26013 EditorMode::full(),
26014 multi_buffer,
26015 Some(project.clone()),
26016 window,
26017 cx,
26018 )
26019 });
26020
26021 editor.update_in(cx, |editor, window, cx| {
26022 let snapshot = editor.buffer().read(cx).snapshot(cx);
26023 editor.runnables.insert(
26024 buffer.read(cx).remote_id(),
26025 3,
26026 buffer.read(cx).version(),
26027 RunnableTasks {
26028 templates: Vec::new(),
26029 offset: snapshot.anchor_before(MultiBufferOffset(43)),
26030 column: 0,
26031 extra_variables: HashMap::default(),
26032 context_range: BufferOffset(43)..BufferOffset(85),
26033 },
26034 );
26035 editor.runnables.insert(
26036 buffer.read(cx).remote_id(),
26037 8,
26038 buffer.read(cx).version(),
26039 RunnableTasks {
26040 templates: Vec::new(),
26041 offset: snapshot.anchor_before(MultiBufferOffset(86)),
26042 column: 0,
26043 extra_variables: HashMap::default(),
26044 context_range: BufferOffset(86)..BufferOffset(191),
26045 },
26046 );
26047
26048 // Test finding task when cursor is inside function body
26049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26050 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
26051 });
26052 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
26053 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
26054
26055 // Test finding task when cursor is on function name
26056 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26057 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
26058 });
26059 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
26060 assert_eq!(row, 8, "Should find task when cursor is on function name");
26061 });
26062}
26063
26064#[gpui::test]
26065async fn test_folding_buffers(cx: &mut TestAppContext) {
26066 init_test(cx, |_| {});
26067
26068 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
26069 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
26070 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
26071
26072 let fs = FakeFs::new(cx.executor());
26073 fs.insert_tree(
26074 path!("/a"),
26075 json!({
26076 "first.rs": sample_text_1,
26077 "second.rs": sample_text_2,
26078 "third.rs": sample_text_3,
26079 }),
26080 )
26081 .await;
26082 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26083 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26084 let cx = &mut VisualTestContext::from_window(*window, cx);
26085 let worktree = project.update(cx, |project, cx| {
26086 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26087 assert_eq!(worktrees.len(), 1);
26088 worktrees.pop().unwrap()
26089 });
26090 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26091
26092 let buffer_1 = project
26093 .update(cx, |project, cx| {
26094 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
26095 })
26096 .await
26097 .unwrap();
26098 let buffer_2 = project
26099 .update(cx, |project, cx| {
26100 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
26101 })
26102 .await
26103 .unwrap();
26104 let buffer_3 = project
26105 .update(cx, |project, cx| {
26106 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
26107 })
26108 .await
26109 .unwrap();
26110
26111 let multi_buffer = cx.new(|cx| {
26112 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26113 multi_buffer.set_excerpts_for_path(
26114 PathKey::sorted(0),
26115 buffer_1.clone(),
26116 [
26117 Point::new(0, 0)..Point::new(2, 0),
26118 Point::new(5, 0)..Point::new(6, 0),
26119 Point::new(9, 0)..Point::new(10, 4),
26120 ],
26121 0,
26122 cx,
26123 );
26124 multi_buffer.set_excerpts_for_path(
26125 PathKey::sorted(1),
26126 buffer_2.clone(),
26127 [
26128 Point::new(0, 0)..Point::new(2, 0),
26129 Point::new(5, 0)..Point::new(6, 0),
26130 Point::new(9, 0)..Point::new(10, 4),
26131 ],
26132 0,
26133 cx,
26134 );
26135 multi_buffer.set_excerpts_for_path(
26136 PathKey::sorted(2),
26137 buffer_3.clone(),
26138 [
26139 Point::new(0, 0)..Point::new(2, 0),
26140 Point::new(5, 0)..Point::new(6, 0),
26141 Point::new(9, 0)..Point::new(10, 4),
26142 ],
26143 0,
26144 cx,
26145 );
26146 multi_buffer
26147 });
26148 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26149 Editor::new(
26150 EditorMode::full(),
26151 multi_buffer.clone(),
26152 Some(project.clone()),
26153 window,
26154 cx,
26155 )
26156 });
26157
26158 assert_eq!(
26159 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26160 "\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",
26161 );
26162
26163 multi_buffer_editor.update(cx, |editor, cx| {
26164 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
26165 });
26166 assert_eq!(
26167 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26168 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26169 "After folding the first buffer, its text should not be displayed"
26170 );
26171
26172 multi_buffer_editor.update(cx, |editor, cx| {
26173 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
26174 });
26175 assert_eq!(
26176 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26177 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
26178 "After folding the second buffer, its text should not be displayed"
26179 );
26180
26181 multi_buffer_editor.update(cx, |editor, cx| {
26182 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
26183 });
26184 assert_eq!(
26185 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26186 "\n\n\n\n\n",
26187 "After folding the third buffer, its text should not be displayed"
26188 );
26189
26190 // Emulate selection inside the fold logic, that should work
26191 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26192 editor
26193 .snapshot(window, cx)
26194 .next_line_boundary(Point::new(0, 4));
26195 });
26196
26197 multi_buffer_editor.update(cx, |editor, cx| {
26198 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
26199 });
26200 assert_eq!(
26201 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26202 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
26203 "After unfolding the second buffer, its text should be displayed"
26204 );
26205
26206 // Typing inside of buffer 1 causes that buffer to be unfolded.
26207 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26208 assert_eq!(
26209 multi_buffer
26210 .read(cx)
26211 .snapshot(cx)
26212 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
26213 .collect::<String>(),
26214 "bbbb"
26215 );
26216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
26217 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
26218 });
26219 editor.handle_input("B", window, cx);
26220 });
26221
26222 assert_eq!(
26223 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26224 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
26225 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
26226 );
26227
26228 multi_buffer_editor.update(cx, |editor, cx| {
26229 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
26230 });
26231 assert_eq!(
26232 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26233 "\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",
26234 "After unfolding the all buffers, all original text should be displayed"
26235 );
26236}
26237
26238#[gpui::test]
26239async fn test_folded_buffers_cleared_on_excerpts_removed(cx: &mut TestAppContext) {
26240 init_test(cx, |_| {});
26241
26242 let fs = FakeFs::new(cx.executor());
26243 fs.insert_tree(
26244 path!("/root"),
26245 json!({
26246 "file_a.txt": "File A\nFile A\nFile A",
26247 "file_b.txt": "File B\nFile B\nFile B",
26248 }),
26249 )
26250 .await;
26251
26252 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
26253 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26254 let cx = &mut VisualTestContext::from_window(*window, cx);
26255 let worktree = project.update(cx, |project, cx| {
26256 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26257 assert_eq!(worktrees.len(), 1);
26258 worktrees.pop().unwrap()
26259 });
26260 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26261
26262 let buffer_a = project
26263 .update(cx, |project, cx| {
26264 project.open_buffer((worktree_id, rel_path("file_a.txt")), cx)
26265 })
26266 .await
26267 .unwrap();
26268 let buffer_b = project
26269 .update(cx, |project, cx| {
26270 project.open_buffer((worktree_id, rel_path("file_b.txt")), cx)
26271 })
26272 .await
26273 .unwrap();
26274
26275 let multi_buffer = cx.new(|cx| {
26276 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26277 let range_a = Point::new(0, 0)..Point::new(2, 4);
26278 let range_b = Point::new(0, 0)..Point::new(2, 4);
26279
26280 multi_buffer.set_excerpts_for_path(PathKey::sorted(0), buffer_a.clone(), [range_a], 0, cx);
26281 multi_buffer.set_excerpts_for_path(PathKey::sorted(1), buffer_b.clone(), [range_b], 0, cx);
26282 multi_buffer
26283 });
26284
26285 let editor = cx.new_window_entity(|window, cx| {
26286 Editor::new(
26287 EditorMode::full(),
26288 multi_buffer.clone(),
26289 Some(project.clone()),
26290 window,
26291 cx,
26292 )
26293 });
26294
26295 editor.update(cx, |editor, cx| {
26296 editor.fold_buffer(buffer_a.read(cx).remote_id(), cx);
26297 });
26298 assert!(editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
26299
26300 // When the excerpts for `buffer_a` are removed, a
26301 // `multi_buffer::Event::ExcerptsRemoved` event is emitted, which should be
26302 // picked up by the editor and update the display map accordingly.
26303 multi_buffer.update(cx, |multi_buffer, cx| {
26304 multi_buffer.remove_excerpts(PathKey::sorted(0), cx)
26305 });
26306 assert!(!editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
26307}
26308
26309#[gpui::test]
26310async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
26311 init_test(cx, |_| {});
26312
26313 let sample_text_1 = "1111\n2222\n3333".to_string();
26314 let sample_text_2 = "4444\n5555\n6666".to_string();
26315 let sample_text_3 = "7777\n8888\n9999".to_string();
26316
26317 let fs = FakeFs::new(cx.executor());
26318 fs.insert_tree(
26319 path!("/a"),
26320 json!({
26321 "first.rs": sample_text_1,
26322 "second.rs": sample_text_2,
26323 "third.rs": sample_text_3,
26324 }),
26325 )
26326 .await;
26327 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26328 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26329 let cx = &mut VisualTestContext::from_window(*window, cx);
26330 let worktree = project.update(cx, |project, cx| {
26331 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26332 assert_eq!(worktrees.len(), 1);
26333 worktrees.pop().unwrap()
26334 });
26335 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26336
26337 let buffer_1 = project
26338 .update(cx, |project, cx| {
26339 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
26340 })
26341 .await
26342 .unwrap();
26343 let buffer_2 = project
26344 .update(cx, |project, cx| {
26345 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
26346 })
26347 .await
26348 .unwrap();
26349 let buffer_3 = project
26350 .update(cx, |project, cx| {
26351 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
26352 })
26353 .await
26354 .unwrap();
26355
26356 let multi_buffer = cx.new(|cx| {
26357 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26358 multi_buffer.set_excerpts_for_path(
26359 PathKey::sorted(0),
26360 buffer_1.clone(),
26361 [Point::new(0, 0)..Point::new(3, 0)],
26362 0,
26363 cx,
26364 );
26365 multi_buffer.set_excerpts_for_path(
26366 PathKey::sorted(1),
26367 buffer_2.clone(),
26368 [Point::new(0, 0)..Point::new(3, 0)],
26369 0,
26370 cx,
26371 );
26372 multi_buffer.set_excerpts_for_path(
26373 PathKey::sorted(2),
26374 buffer_3.clone(),
26375 [Point::new(0, 0)..Point::new(3, 0)],
26376 0,
26377 cx,
26378 );
26379 multi_buffer
26380 });
26381
26382 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26383 Editor::new(
26384 EditorMode::full(),
26385 multi_buffer,
26386 Some(project.clone()),
26387 window,
26388 cx,
26389 )
26390 });
26391
26392 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
26393 assert_eq!(
26394 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26395 full_text,
26396 );
26397
26398 multi_buffer_editor.update(cx, |editor, cx| {
26399 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
26400 });
26401 assert_eq!(
26402 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26403 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
26404 "After folding the first buffer, its text should not be displayed"
26405 );
26406
26407 multi_buffer_editor.update(cx, |editor, cx| {
26408 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
26409 });
26410
26411 assert_eq!(
26412 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26413 "\n\n\n\n\n\n7777\n8888\n9999",
26414 "After folding the second buffer, its text should not be displayed"
26415 );
26416
26417 multi_buffer_editor.update(cx, |editor, cx| {
26418 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
26419 });
26420 assert_eq!(
26421 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26422 "\n\n\n\n\n",
26423 "After folding the third buffer, its text should not be displayed"
26424 );
26425
26426 multi_buffer_editor.update(cx, |editor, cx| {
26427 editor.unfold_buffer(buffer_2.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",
26432 "After unfolding the second buffer, its text should be displayed"
26433 );
26434
26435 multi_buffer_editor.update(cx, |editor, cx| {
26436 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
26437 });
26438 assert_eq!(
26439 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26440 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
26441 "After unfolding the first buffer, its text should be displayed"
26442 );
26443
26444 multi_buffer_editor.update(cx, |editor, cx| {
26445 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
26446 });
26447 assert_eq!(
26448 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26449 full_text,
26450 "After unfolding all buffers, all original text should be displayed"
26451 );
26452}
26453
26454#[gpui::test]
26455async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
26456 init_test(cx, |_| {});
26457
26458 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
26459
26460 let fs = FakeFs::new(cx.executor());
26461 fs.insert_tree(
26462 path!("/a"),
26463 json!({
26464 "main.rs": sample_text,
26465 }),
26466 )
26467 .await;
26468 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26469 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26470 let cx = &mut VisualTestContext::from_window(*window, cx);
26471 let worktree = project.update(cx, |project, cx| {
26472 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
26473 assert_eq!(worktrees.len(), 1);
26474 worktrees.pop().unwrap()
26475 });
26476 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
26477
26478 let buffer_1 = project
26479 .update(cx, |project, cx| {
26480 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
26481 })
26482 .await
26483 .unwrap();
26484
26485 let multi_buffer = cx.new(|cx| {
26486 let mut multi_buffer = MultiBuffer::new(ReadWrite);
26487 multi_buffer.set_excerpts_for_path(
26488 PathKey::sorted(0),
26489 buffer_1.clone(),
26490 [Point::new(0, 0)
26491 ..Point::new(
26492 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
26493 0,
26494 )],
26495 0,
26496 cx,
26497 );
26498 multi_buffer
26499 });
26500 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
26501 Editor::new(
26502 EditorMode::full(),
26503 multi_buffer,
26504 Some(project.clone()),
26505 window,
26506 cx,
26507 )
26508 });
26509
26510 let selection_range = Point::new(1, 0)..Point::new(2, 0);
26511 multi_buffer_editor.update_in(cx, |editor, window, cx| {
26512 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26513 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
26514 editor.highlight_text(
26515 HighlightKey::Editor,
26516 vec![highlight_range.clone()],
26517 HighlightStyle::color(Hsla::green()),
26518 cx,
26519 );
26520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26521 s.select_ranges(Some(highlight_range))
26522 });
26523 });
26524
26525 let full_text = format!("\n\n{sample_text}");
26526 assert_eq!(
26527 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
26528 full_text,
26529 );
26530}
26531
26532#[gpui::test]
26533async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
26534 init_test(cx, |_| {});
26535 cx.update(|cx| {
26536 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
26537 "keymaps/default-linux.json",
26538 cx,
26539 )
26540 .unwrap();
26541 cx.bind_keys(default_key_bindings);
26542 });
26543
26544 let (editor, cx) = cx.add_window_view(|window, cx| {
26545 let multi_buffer = MultiBuffer::build_multi(
26546 [
26547 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
26548 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
26549 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
26550 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
26551 ],
26552 cx,
26553 );
26554 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26555
26556 let buffer_ids = multi_buffer
26557 .read(cx)
26558 .snapshot(cx)
26559 .excerpts()
26560 .map(|excerpt| excerpt.context.start.buffer_id)
26561 .collect::<Vec<_>>();
26562 // fold all but the second buffer, so that we test navigating between two
26563 // adjacent folded buffers, as well as folded buffers at the start and
26564 // end the multibuffer
26565 editor.fold_buffer(buffer_ids[0], cx);
26566 editor.fold_buffer(buffer_ids[2], cx);
26567 editor.fold_buffer(buffer_ids[3], cx);
26568
26569 editor
26570 });
26571 cx.simulate_resize(size(px(1000.), px(1000.)));
26572
26573 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26574 cx.assert_excerpts_with_selections(indoc! {"
26575 [EXCERPT]
26576 ˇ[FOLDED]
26577 [EXCERPT]
26578 a1
26579 b1
26580 [EXCERPT]
26581 [FOLDED]
26582 [EXCERPT]
26583 [FOLDED]
26584 "
26585 });
26586 cx.simulate_keystroke("down");
26587 cx.assert_excerpts_with_selections(indoc! {"
26588 [EXCERPT]
26589 [FOLDED]
26590 [EXCERPT]
26591 ˇa1
26592 b1
26593 [EXCERPT]
26594 [FOLDED]
26595 [EXCERPT]
26596 [FOLDED]
26597 "
26598 });
26599 cx.simulate_keystroke("down");
26600 cx.assert_excerpts_with_selections(indoc! {"
26601 [EXCERPT]
26602 [FOLDED]
26603 [EXCERPT]
26604 a1
26605 ˇb1
26606 [EXCERPT]
26607 [FOLDED]
26608 [EXCERPT]
26609 [FOLDED]
26610 "
26611 });
26612 cx.simulate_keystroke("down");
26613 cx.assert_excerpts_with_selections(indoc! {"
26614 [EXCERPT]
26615 [FOLDED]
26616 [EXCERPT]
26617 a1
26618 b1
26619 ˇ[EXCERPT]
26620 [FOLDED]
26621 [EXCERPT]
26622 [FOLDED]
26623 "
26624 });
26625 cx.simulate_keystroke("down");
26626 cx.assert_excerpts_with_selections(indoc! {"
26627 [EXCERPT]
26628 [FOLDED]
26629 [EXCERPT]
26630 a1
26631 b1
26632 [EXCERPT]
26633 ˇ[FOLDED]
26634 [EXCERPT]
26635 [FOLDED]
26636 "
26637 });
26638 for _ in 0..5 {
26639 cx.simulate_keystroke("down");
26640 cx.assert_excerpts_with_selections(indoc! {"
26641 [EXCERPT]
26642 [FOLDED]
26643 [EXCERPT]
26644 a1
26645 b1
26646 [EXCERPT]
26647 [FOLDED]
26648 [EXCERPT]
26649 ˇ[FOLDED]
26650 "
26651 });
26652 }
26653
26654 cx.simulate_keystroke("up");
26655 cx.assert_excerpts_with_selections(indoc! {"
26656 [EXCERPT]
26657 [FOLDED]
26658 [EXCERPT]
26659 a1
26660 b1
26661 [EXCERPT]
26662 ˇ[FOLDED]
26663 [EXCERPT]
26664 [FOLDED]
26665 "
26666 });
26667 cx.simulate_keystroke("up");
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 cx.simulate_keystroke("up");
26681 cx.assert_excerpts_with_selections(indoc! {"
26682 [EXCERPT]
26683 [FOLDED]
26684 [EXCERPT]
26685 a1
26686 ˇb1
26687 [EXCERPT]
26688 [FOLDED]
26689 [EXCERPT]
26690 [FOLDED]
26691 "
26692 });
26693 cx.simulate_keystroke("up");
26694 cx.assert_excerpts_with_selections(indoc! {"
26695 [EXCERPT]
26696 [FOLDED]
26697 [EXCERPT]
26698 ˇa1
26699 b1
26700 [EXCERPT]
26701 [FOLDED]
26702 [EXCERPT]
26703 [FOLDED]
26704 "
26705 });
26706 for _ in 0..5 {
26707 cx.simulate_keystroke("up");
26708 cx.assert_excerpts_with_selections(indoc! {"
26709 [EXCERPT]
26710 ˇ[FOLDED]
26711 [EXCERPT]
26712 a1
26713 b1
26714 [EXCERPT]
26715 [FOLDED]
26716 [EXCERPT]
26717 [FOLDED]
26718 "
26719 });
26720 }
26721}
26722
26723#[gpui::test]
26724async fn test_edit_prediction_text(cx: &mut TestAppContext) {
26725 init_test(cx, |_| {});
26726
26727 // Simple insertion
26728 assert_highlighted_edits(
26729 "Hello, world!",
26730 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
26731 true,
26732 cx,
26733 &|highlighted_edits, cx| {
26734 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
26735 assert_eq!(highlighted_edits.highlights.len(), 1);
26736 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
26737 assert_eq!(
26738 highlighted_edits.highlights[0].1.background_color,
26739 Some(cx.theme().status().created_background)
26740 );
26741 },
26742 )
26743 .await;
26744
26745 // Replacement
26746 assert_highlighted_edits(
26747 "This is a test.",
26748 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
26749 false,
26750 cx,
26751 &|highlighted_edits, cx| {
26752 assert_eq!(highlighted_edits.text, "That is a test.");
26753 assert_eq!(highlighted_edits.highlights.len(), 1);
26754 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
26755 assert_eq!(
26756 highlighted_edits.highlights[0].1.background_color,
26757 Some(cx.theme().status().created_background)
26758 );
26759 },
26760 )
26761 .await;
26762
26763 // Multiple edits
26764 assert_highlighted_edits(
26765 "Hello, world!",
26766 vec![
26767 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
26768 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
26769 ],
26770 false,
26771 cx,
26772 &|highlighted_edits, cx| {
26773 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
26774 assert_eq!(highlighted_edits.highlights.len(), 2);
26775 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
26776 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
26777 assert_eq!(
26778 highlighted_edits.highlights[0].1.background_color,
26779 Some(cx.theme().status().created_background)
26780 );
26781 assert_eq!(
26782 highlighted_edits.highlights[1].1.background_color,
26783 Some(cx.theme().status().created_background)
26784 );
26785 },
26786 )
26787 .await;
26788
26789 // Multiple lines with edits
26790 assert_highlighted_edits(
26791 "First line\nSecond line\nThird line\nFourth line",
26792 vec![
26793 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
26794 (
26795 Point::new(2, 0)..Point::new(2, 10),
26796 "New third line".to_string(),
26797 ),
26798 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
26799 ],
26800 false,
26801 cx,
26802 &|highlighted_edits, cx| {
26803 assert_eq!(
26804 highlighted_edits.text,
26805 "Second modified\nNew third line\nFourth updated line"
26806 );
26807 assert_eq!(highlighted_edits.highlights.len(), 3);
26808 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
26809 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
26810 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
26811 for highlight in &highlighted_edits.highlights {
26812 assert_eq!(
26813 highlight.1.background_color,
26814 Some(cx.theme().status().created_background)
26815 );
26816 }
26817 },
26818 )
26819 .await;
26820}
26821
26822#[gpui::test]
26823async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
26824 init_test(cx, |_| {});
26825
26826 // Deletion
26827 assert_highlighted_edits(
26828 "Hello, world!",
26829 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
26830 true,
26831 cx,
26832 &|highlighted_edits, cx| {
26833 assert_eq!(highlighted_edits.text, "Hello, world!");
26834 assert_eq!(highlighted_edits.highlights.len(), 1);
26835 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
26836 assert_eq!(
26837 highlighted_edits.highlights[0].1.background_color,
26838 Some(cx.theme().status().deleted_background)
26839 );
26840 },
26841 )
26842 .await;
26843
26844 // Insertion
26845 assert_highlighted_edits(
26846 "Hello, world!",
26847 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
26848 true,
26849 cx,
26850 &|highlighted_edits, cx| {
26851 assert_eq!(highlighted_edits.highlights.len(), 1);
26852 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
26853 assert_eq!(
26854 highlighted_edits.highlights[0].1.background_color,
26855 Some(cx.theme().status().created_background)
26856 );
26857 },
26858 )
26859 .await;
26860}
26861
26862async fn assert_highlighted_edits(
26863 text: &str,
26864 edits: Vec<(Range<Point>, String)>,
26865 include_deletions: bool,
26866 cx: &mut TestAppContext,
26867 assertion_fn: &dyn Fn(HighlightedText, &App),
26868) {
26869 let window = cx.add_window(|window, cx| {
26870 let buffer = MultiBuffer::build_simple(text, cx);
26871 Editor::new(EditorMode::full(), buffer, None, window, cx)
26872 });
26873 let cx = &mut VisualTestContext::from_window(*window, cx);
26874
26875 let (buffer, snapshot) = window
26876 .update(cx, |editor, _window, cx| {
26877 (
26878 editor.buffer().clone(),
26879 editor.buffer().read(cx).snapshot(cx),
26880 )
26881 })
26882 .unwrap();
26883
26884 let edits = edits
26885 .into_iter()
26886 .map(|(range, edit)| {
26887 (
26888 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
26889 edit,
26890 )
26891 })
26892 .collect::<Vec<_>>();
26893
26894 let text_anchor_edits = edits
26895 .clone()
26896 .into_iter()
26897 .map(|(range, edit)| {
26898 (
26899 range.start.expect_text_anchor()..range.end.expect_text_anchor(),
26900 edit.into(),
26901 )
26902 })
26903 .collect::<Vec<_>>();
26904
26905 let edit_preview = window
26906 .update(cx, |_, _window, cx| {
26907 buffer
26908 .read(cx)
26909 .as_singleton()
26910 .unwrap()
26911 .read(cx)
26912 .preview_edits(text_anchor_edits.into(), cx)
26913 })
26914 .unwrap()
26915 .await;
26916
26917 cx.update(|_window, cx| {
26918 let highlighted_edits = edit_prediction_edit_text(
26919 snapshot.as_singleton().unwrap(),
26920 &edits,
26921 &edit_preview,
26922 include_deletions,
26923 &snapshot,
26924 cx,
26925 );
26926 assertion_fn(highlighted_edits, cx)
26927 });
26928}
26929
26930#[track_caller]
26931fn assert_breakpoint(
26932 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
26933 path: &Arc<Path>,
26934 expected: Vec<(u32, Breakpoint)>,
26935) {
26936 if expected.is_empty() {
26937 assert!(!breakpoints.contains_key(path), "{}", path.display());
26938 } else {
26939 let mut breakpoint = breakpoints
26940 .get(path)
26941 .unwrap()
26942 .iter()
26943 .map(|breakpoint| {
26944 (
26945 breakpoint.row,
26946 Breakpoint {
26947 message: breakpoint.message.clone(),
26948 state: breakpoint.state,
26949 condition: breakpoint.condition.clone(),
26950 hit_condition: breakpoint.hit_condition.clone(),
26951 },
26952 )
26953 })
26954 .collect::<Vec<_>>();
26955
26956 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
26957
26958 assert_eq!(expected, breakpoint);
26959 }
26960}
26961
26962fn add_log_breakpoint_at_cursor(
26963 editor: &mut Editor,
26964 log_message: &str,
26965 window: &mut Window,
26966 cx: &mut Context<Editor>,
26967) {
26968 let (anchor, bp) = editor
26969 .breakpoints_at_cursors(window, cx)
26970 .first()
26971 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
26972 .unwrap_or_else(|| {
26973 let snapshot = editor.snapshot(window, cx);
26974 let cursor_position: Point =
26975 editor.selections.newest(&snapshot.display_snapshot).head();
26976
26977 let breakpoint_position = snapshot
26978 .buffer_snapshot()
26979 .anchor_before(Point::new(cursor_position.row, 0));
26980
26981 (breakpoint_position, Breakpoint::new_log(log_message))
26982 });
26983
26984 editor.edit_breakpoint_at_anchor(
26985 anchor,
26986 bp,
26987 BreakpointEditAction::EditLogMessage(log_message.into()),
26988 cx,
26989 );
26990}
26991
26992#[gpui::test]
26993async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
26994 init_test(cx, |_| {});
26995
26996 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
26997 let fs = FakeFs::new(cx.executor());
26998 fs.insert_tree(
26999 path!("/a"),
27000 json!({
27001 "main.rs": sample_text,
27002 }),
27003 )
27004 .await;
27005 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27006 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27007 let cx = &mut VisualTestContext::from_window(*window, cx);
27008
27009 let fs = FakeFs::new(cx.executor());
27010 fs.insert_tree(
27011 path!("/a"),
27012 json!({
27013 "main.rs": sample_text,
27014 }),
27015 )
27016 .await;
27017 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27018 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27019 let workspace = window
27020 .read_with(cx, |mw, _| mw.workspace().clone())
27021 .unwrap();
27022 let cx = &mut VisualTestContext::from_window(*window, cx);
27023 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27024 workspace.project().update(cx, |project, cx| {
27025 project.worktrees(cx).next().unwrap().read(cx).id()
27026 })
27027 });
27028
27029 let buffer = project
27030 .update(cx, |project, cx| {
27031 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27032 })
27033 .await
27034 .unwrap();
27035
27036 let (editor, cx) = cx.add_window_view(|window, cx| {
27037 Editor::new(
27038 EditorMode::full(),
27039 MultiBuffer::build_from_buffer(buffer, cx),
27040 Some(project.clone()),
27041 window,
27042 cx,
27043 )
27044 });
27045
27046 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27047 let abs_path = project.read_with(cx, |project, cx| {
27048 project
27049 .absolute_path(&project_path, cx)
27050 .map(Arc::from)
27051 .unwrap()
27052 });
27053
27054 // assert we can add breakpoint on the first line
27055 editor.update_in(cx, |editor, window, cx| {
27056 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27057 editor.move_to_end(&MoveToEnd, window, cx);
27058 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27059 });
27060
27061 let breakpoints = editor.update(cx, |editor, cx| {
27062 editor
27063 .breakpoint_store()
27064 .as_ref()
27065 .unwrap()
27066 .read(cx)
27067 .all_source_breakpoints(cx)
27068 });
27069
27070 assert_eq!(1, breakpoints.len());
27071 assert_breakpoint(
27072 &breakpoints,
27073 &abs_path,
27074 vec![
27075 (0, Breakpoint::new_standard()),
27076 (3, Breakpoint::new_standard()),
27077 ],
27078 );
27079
27080 editor.update_in(cx, |editor, window, cx| {
27081 editor.move_to_beginning(&MoveToBeginning, window, cx);
27082 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27083 });
27084
27085 let breakpoints = editor.update(cx, |editor, cx| {
27086 editor
27087 .breakpoint_store()
27088 .as_ref()
27089 .unwrap()
27090 .read(cx)
27091 .all_source_breakpoints(cx)
27092 });
27093
27094 assert_eq!(1, breakpoints.len());
27095 assert_breakpoint(
27096 &breakpoints,
27097 &abs_path,
27098 vec![(3, Breakpoint::new_standard())],
27099 );
27100
27101 editor.update_in(cx, |editor, window, cx| {
27102 editor.move_to_end(&MoveToEnd, window, cx);
27103 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27104 });
27105
27106 let breakpoints = editor.update(cx, |editor, cx| {
27107 editor
27108 .breakpoint_store()
27109 .as_ref()
27110 .unwrap()
27111 .read(cx)
27112 .all_source_breakpoints(cx)
27113 });
27114
27115 assert_eq!(0, breakpoints.len());
27116 assert_breakpoint(&breakpoints, &abs_path, vec![]);
27117}
27118
27119#[gpui::test]
27120async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
27121 init_test(cx, |_| {});
27122
27123 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27124
27125 let fs = FakeFs::new(cx.executor());
27126 fs.insert_tree(
27127 path!("/a"),
27128 json!({
27129 "main.rs": sample_text,
27130 }),
27131 )
27132 .await;
27133 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27134 let (multi_workspace, cx) =
27135 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27136 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27137
27138 let worktree_id = workspace.update(cx, |workspace, cx| {
27139 workspace.project().update(cx, |project, cx| {
27140 project.worktrees(cx).next().unwrap().read(cx).id()
27141 })
27142 });
27143
27144 let buffer = project
27145 .update(cx, |project, cx| {
27146 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27147 })
27148 .await
27149 .unwrap();
27150
27151 let (editor, cx) = cx.add_window_view(|window, cx| {
27152 Editor::new(
27153 EditorMode::full(),
27154 MultiBuffer::build_from_buffer(buffer, cx),
27155 Some(project.clone()),
27156 window,
27157 cx,
27158 )
27159 });
27160
27161 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27162 let abs_path = project.read_with(cx, |project, cx| {
27163 project
27164 .absolute_path(&project_path, cx)
27165 .map(Arc::from)
27166 .unwrap()
27167 });
27168
27169 editor.update_in(cx, |editor, window, cx| {
27170 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
27171 });
27172
27173 let breakpoints = editor.update(cx, |editor, cx| {
27174 editor
27175 .breakpoint_store()
27176 .as_ref()
27177 .unwrap()
27178 .read(cx)
27179 .all_source_breakpoints(cx)
27180 });
27181
27182 assert_breakpoint(
27183 &breakpoints,
27184 &abs_path,
27185 vec![(0, Breakpoint::new_log("hello world"))],
27186 );
27187
27188 // Removing a log message from a log breakpoint should remove it
27189 editor.update_in(cx, |editor, window, cx| {
27190 add_log_breakpoint_at_cursor(editor, "", window, cx);
27191 });
27192
27193 let breakpoints = editor.update(cx, |editor, cx| {
27194 editor
27195 .breakpoint_store()
27196 .as_ref()
27197 .unwrap()
27198 .read(cx)
27199 .all_source_breakpoints(cx)
27200 });
27201
27202 assert_breakpoint(&breakpoints, &abs_path, vec![]);
27203
27204 editor.update_in(cx, |editor, window, cx| {
27205 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27206 editor.move_to_end(&MoveToEnd, window, cx);
27207 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27208 // Not adding a log message to a standard breakpoint shouldn't remove it
27209 add_log_breakpoint_at_cursor(editor, "", window, cx);
27210 });
27211
27212 let breakpoints = editor.update(cx, |editor, cx| {
27213 editor
27214 .breakpoint_store()
27215 .as_ref()
27216 .unwrap()
27217 .read(cx)
27218 .all_source_breakpoints(cx)
27219 });
27220
27221 assert_breakpoint(
27222 &breakpoints,
27223 &abs_path,
27224 vec![
27225 (0, Breakpoint::new_standard()),
27226 (3, Breakpoint::new_standard()),
27227 ],
27228 );
27229
27230 editor.update_in(cx, |editor, window, cx| {
27231 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
27232 });
27233
27234 let breakpoints = editor.update(cx, |editor, cx| {
27235 editor
27236 .breakpoint_store()
27237 .as_ref()
27238 .unwrap()
27239 .read(cx)
27240 .all_source_breakpoints(cx)
27241 });
27242
27243 assert_breakpoint(
27244 &breakpoints,
27245 &abs_path,
27246 vec![
27247 (0, Breakpoint::new_standard()),
27248 (3, Breakpoint::new_log("hello world")),
27249 ],
27250 );
27251
27252 editor.update_in(cx, |editor, window, cx| {
27253 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
27254 });
27255
27256 let breakpoints = editor.update(cx, |editor, cx| {
27257 editor
27258 .breakpoint_store()
27259 .as_ref()
27260 .unwrap()
27261 .read(cx)
27262 .all_source_breakpoints(cx)
27263 });
27264
27265 assert_breakpoint(
27266 &breakpoints,
27267 &abs_path,
27268 vec![
27269 (0, Breakpoint::new_standard()),
27270 (3, Breakpoint::new_log("hello Earth!!")),
27271 ],
27272 );
27273}
27274
27275/// This also tests that Editor::breakpoint_at_cursor_head is working properly
27276/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
27277/// or when breakpoints were placed out of order. This tests for a regression too
27278#[gpui::test]
27279async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
27280 init_test(cx, |_| {});
27281
27282 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27283 let fs = FakeFs::new(cx.executor());
27284 fs.insert_tree(
27285 path!("/a"),
27286 json!({
27287 "main.rs": sample_text,
27288 }),
27289 )
27290 .await;
27291 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27292 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27293 let cx = &mut VisualTestContext::from_window(*window, cx);
27294
27295 let fs = FakeFs::new(cx.executor());
27296 fs.insert_tree(
27297 path!("/a"),
27298 json!({
27299 "main.rs": sample_text,
27300 }),
27301 )
27302 .await;
27303 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27304 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27305 let workspace = window
27306 .read_with(cx, |mw, _| mw.workspace().clone())
27307 .unwrap();
27308 let cx = &mut VisualTestContext::from_window(*window, cx);
27309 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27310 workspace.project().update(cx, |project, cx| {
27311 project.worktrees(cx).next().unwrap().read(cx).id()
27312 })
27313 });
27314
27315 let buffer = project
27316 .update(cx, |project, cx| {
27317 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27318 })
27319 .await
27320 .unwrap();
27321
27322 let (editor, cx) = cx.add_window_view(|window, cx| {
27323 Editor::new(
27324 EditorMode::full(),
27325 MultiBuffer::build_from_buffer(buffer, cx),
27326 Some(project.clone()),
27327 window,
27328 cx,
27329 )
27330 });
27331
27332 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
27333 let abs_path = project.read_with(cx, |project, cx| {
27334 project
27335 .absolute_path(&project_path, cx)
27336 .map(Arc::from)
27337 .unwrap()
27338 });
27339
27340 // assert we can add breakpoint on the first line
27341 editor.update_in(cx, |editor, window, cx| {
27342 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27343 editor.move_to_end(&MoveToEnd, window, cx);
27344 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27345 editor.move_up(&MoveUp, window, cx);
27346 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27347 });
27348
27349 let breakpoints = editor.update(cx, |editor, cx| {
27350 editor
27351 .breakpoint_store()
27352 .as_ref()
27353 .unwrap()
27354 .read(cx)
27355 .all_source_breakpoints(cx)
27356 });
27357
27358 assert_eq!(1, breakpoints.len());
27359 assert_breakpoint(
27360 &breakpoints,
27361 &abs_path,
27362 vec![
27363 (0, Breakpoint::new_standard()),
27364 (2, Breakpoint::new_standard()),
27365 (3, Breakpoint::new_standard()),
27366 ],
27367 );
27368
27369 editor.update_in(cx, |editor, window, cx| {
27370 editor.move_to_beginning(&MoveToBeginning, window, cx);
27371 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27372 editor.move_to_end(&MoveToEnd, window, cx);
27373 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27374 // Disabling a breakpoint that doesn't exist should do nothing
27375 editor.move_up(&MoveUp, window, cx);
27376 editor.move_up(&MoveUp, window, cx);
27377 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27378 });
27379
27380 let breakpoints = editor.update(cx, |editor, cx| {
27381 editor
27382 .breakpoint_store()
27383 .as_ref()
27384 .unwrap()
27385 .read(cx)
27386 .all_source_breakpoints(cx)
27387 });
27388
27389 let disable_breakpoint = {
27390 let mut bp = Breakpoint::new_standard();
27391 bp.state = BreakpointState::Disabled;
27392 bp
27393 };
27394
27395 assert_eq!(1, breakpoints.len());
27396 assert_breakpoint(
27397 &breakpoints,
27398 &abs_path,
27399 vec![
27400 (0, disable_breakpoint.clone()),
27401 (2, Breakpoint::new_standard()),
27402 (3, disable_breakpoint.clone()),
27403 ],
27404 );
27405
27406 editor.update_in(cx, |editor, window, cx| {
27407 editor.move_to_beginning(&MoveToBeginning, window, cx);
27408 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
27409 editor.move_to_end(&MoveToEnd, window, cx);
27410 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
27411 editor.move_up(&MoveUp, window, cx);
27412 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
27413 });
27414
27415 let breakpoints = editor.update(cx, |editor, cx| {
27416 editor
27417 .breakpoint_store()
27418 .as_ref()
27419 .unwrap()
27420 .read(cx)
27421 .all_source_breakpoints(cx)
27422 });
27423
27424 assert_eq!(1, breakpoints.len());
27425 assert_breakpoint(
27426 &breakpoints,
27427 &abs_path,
27428 vec![
27429 (0, Breakpoint::new_standard()),
27430 (2, disable_breakpoint),
27431 (3, Breakpoint::new_standard()),
27432 ],
27433 );
27434}
27435
27436#[gpui::test]
27437async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
27438 init_test(cx, |_| {});
27439
27440 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
27441 let fs = FakeFs::new(cx.executor());
27442 fs.insert_tree(
27443 path!("/a"),
27444 json!({
27445 "main.rs": sample_text,
27446 }),
27447 )
27448 .await;
27449 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27450 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27451 let workspace = window
27452 .read_with(cx, |mw, _| mw.workspace().clone())
27453 .unwrap();
27454 let cx = &mut VisualTestContext::from_window(*window, cx);
27455 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27456 workspace.project().update(cx, |project, cx| {
27457 project.worktrees(cx).next().unwrap().read(cx).id()
27458 })
27459 });
27460
27461 let buffer = project
27462 .update(cx, |project, cx| {
27463 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
27464 })
27465 .await
27466 .unwrap();
27467
27468 let (editor, cx) = cx.add_window_view(|window, cx| {
27469 Editor::new(
27470 EditorMode::full(),
27471 MultiBuffer::build_from_buffer(buffer, cx),
27472 Some(project.clone()),
27473 window,
27474 cx,
27475 )
27476 });
27477
27478 // Simulate hovering over row 0 with no existing breakpoint.
27479 editor.update(cx, |editor, _cx| {
27480 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
27481 display_row: DisplayRow(0),
27482 is_active: true,
27483 collides_with_existing_breakpoint: false,
27484 });
27485 });
27486
27487 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
27488 editor.update_in(cx, |editor, window, cx| {
27489 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27490 });
27491 editor.update(cx, |editor, _cx| {
27492 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27493 assert!(
27494 indicator.collides_with_existing_breakpoint,
27495 "Adding a breakpoint on the hovered row should set collision to true"
27496 );
27497 });
27498
27499 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
27500 editor.update_in(cx, |editor, window, cx| {
27501 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27502 });
27503 editor.update(cx, |editor, _cx| {
27504 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27505 assert!(
27506 !indicator.collides_with_existing_breakpoint,
27507 "Removing a breakpoint on the hovered row should set collision to false"
27508 );
27509 });
27510
27511 // Now move cursor to row 2 while phantom indicator stays on row 0.
27512 editor.update_in(cx, |editor, window, cx| {
27513 editor.move_down(&MoveDown, window, cx);
27514 editor.move_down(&MoveDown, window, cx);
27515 });
27516
27517 // Ensure phantom indicator is still on row 0, not colliding.
27518 editor.update(cx, |editor, _cx| {
27519 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
27520 display_row: DisplayRow(0),
27521 is_active: true,
27522 collides_with_existing_breakpoint: false,
27523 });
27524 });
27525
27526 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
27527 editor.update_in(cx, |editor, window, cx| {
27528 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
27529 });
27530 editor.update(cx, |editor, _cx| {
27531 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
27532 assert!(
27533 !indicator.collides_with_existing_breakpoint,
27534 "Toggling a breakpoint on a different row should not affect the phantom indicator"
27535 );
27536 });
27537}
27538
27539#[gpui::test]
27540async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
27541 init_test(cx, |_| {});
27542 let capabilities = lsp::ServerCapabilities {
27543 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
27544 prepare_provider: Some(true),
27545 work_done_progress_options: Default::default(),
27546 })),
27547 ..Default::default()
27548 };
27549 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
27550
27551 cx.set_state(indoc! {"
27552 struct Fˇoo {}
27553 "});
27554
27555 cx.update_editor(|editor, _, cx| {
27556 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
27557 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
27558 editor.highlight_background(
27559 HighlightKey::DocumentHighlightRead,
27560 &[highlight_range],
27561 |_, theme| theme.colors().editor_document_highlight_read_background,
27562 cx,
27563 );
27564 });
27565
27566 let mut prepare_rename_handler = cx
27567 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
27568 move |_, _, _| async move {
27569 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
27570 start: lsp::Position {
27571 line: 0,
27572 character: 7,
27573 },
27574 end: lsp::Position {
27575 line: 0,
27576 character: 10,
27577 },
27578 })))
27579 },
27580 );
27581 let prepare_rename_task = cx
27582 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
27583 .expect("Prepare rename was not started");
27584 prepare_rename_handler.next().await.unwrap();
27585 prepare_rename_task.await.expect("Prepare rename failed");
27586
27587 let mut rename_handler =
27588 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
27589 let edit = lsp::TextEdit {
27590 range: lsp::Range {
27591 start: lsp::Position {
27592 line: 0,
27593 character: 7,
27594 },
27595 end: lsp::Position {
27596 line: 0,
27597 character: 10,
27598 },
27599 },
27600 new_text: "FooRenamed".to_string(),
27601 };
27602 Ok(Some(lsp::WorkspaceEdit::new(
27603 // Specify the same edit twice
27604 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
27605 )))
27606 });
27607 let rename_task = cx
27608 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
27609 .expect("Confirm rename was not started");
27610 rename_handler.next().await.unwrap();
27611 rename_task.await.expect("Confirm rename failed");
27612 cx.run_until_parked();
27613
27614 // Despite two edits, only one is actually applied as those are identical
27615 cx.assert_editor_state(indoc! {"
27616 struct FooRenamedˇ {}
27617 "});
27618}
27619
27620#[gpui::test]
27621async fn test_rename_without_prepare(cx: &mut TestAppContext) {
27622 init_test(cx, |_| {});
27623 // These capabilities indicate that the server does not support prepare rename.
27624 let capabilities = lsp::ServerCapabilities {
27625 rename_provider: Some(lsp::OneOf::Left(true)),
27626 ..Default::default()
27627 };
27628 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
27629
27630 cx.set_state(indoc! {"
27631 struct Fˇoo {}
27632 "});
27633
27634 cx.update_editor(|editor, _window, cx| {
27635 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
27636 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
27637 editor.highlight_background(
27638 HighlightKey::DocumentHighlightRead,
27639 &[highlight_range],
27640 |_, theme| theme.colors().editor_document_highlight_read_background,
27641 cx,
27642 );
27643 });
27644
27645 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
27646 .expect("Prepare rename was not started")
27647 .await
27648 .expect("Prepare rename failed");
27649
27650 let mut rename_handler =
27651 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
27652 let edit = lsp::TextEdit {
27653 range: lsp::Range {
27654 start: lsp::Position {
27655 line: 0,
27656 character: 7,
27657 },
27658 end: lsp::Position {
27659 line: 0,
27660 character: 10,
27661 },
27662 },
27663 new_text: "FooRenamed".to_string(),
27664 };
27665 Ok(Some(lsp::WorkspaceEdit::new(
27666 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
27667 )))
27668 });
27669 let rename_task = cx
27670 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
27671 .expect("Confirm rename was not started");
27672 rename_handler.next().await.unwrap();
27673 rename_task.await.expect("Confirm rename failed");
27674 cx.run_until_parked();
27675
27676 // Correct range is renamed, as `surrounding_word` is used to find it.
27677 cx.assert_editor_state(indoc! {"
27678 struct FooRenamedˇ {}
27679 "});
27680}
27681
27682#[gpui::test]
27683async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
27684 init_test(cx, |_| {});
27685 let mut cx = EditorTestContext::new(cx).await;
27686
27687 let language = Arc::new(
27688 Language::new(
27689 LanguageConfig::default(),
27690 Some(tree_sitter_html::LANGUAGE.into()),
27691 )
27692 .with_brackets_query(
27693 r#"
27694 ("<" @open "/>" @close)
27695 ("</" @open ">" @close)
27696 ("<" @open ">" @close)
27697 ("\"" @open "\"" @close)
27698 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
27699 "#,
27700 )
27701 .unwrap(),
27702 );
27703 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27704
27705 cx.set_state(indoc! {"
27706 <span>ˇ</span>
27707 "});
27708 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27709 cx.assert_editor_state(indoc! {"
27710 <span>
27711 ˇ
27712 </span>
27713 "});
27714
27715 cx.set_state(indoc! {"
27716 <span><span></span>ˇ</span>
27717 "});
27718 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27719 cx.assert_editor_state(indoc! {"
27720 <span><span></span>
27721 ˇ</span>
27722 "});
27723
27724 cx.set_state(indoc! {"
27725 <span>ˇ
27726 </span>
27727 "});
27728 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
27729 cx.assert_editor_state(indoc! {"
27730 <span>
27731 ˇ
27732 </span>
27733 "});
27734}
27735
27736#[gpui::test(iterations = 10)]
27737async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
27738 init_test(cx, |_| {});
27739
27740 let fs = FakeFs::new(cx.executor());
27741 fs.insert_tree(
27742 path!("/dir"),
27743 json!({
27744 "a.ts": "a",
27745 }),
27746 )
27747 .await;
27748
27749 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
27750 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27751 let workspace = window
27752 .read_with(cx, |mw, _| mw.workspace().clone())
27753 .unwrap();
27754 let cx = &mut VisualTestContext::from_window(*window, cx);
27755
27756 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27757 language_registry.add(Arc::new(Language::new(
27758 LanguageConfig {
27759 name: "TypeScript".into(),
27760 matcher: LanguageMatcher {
27761 path_suffixes: vec!["ts".to_string()],
27762 ..Default::default()
27763 },
27764 ..Default::default()
27765 },
27766 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
27767 )));
27768 let mut fake_language_servers = language_registry.register_fake_lsp(
27769 "TypeScript",
27770 FakeLspAdapter {
27771 capabilities: lsp::ServerCapabilities {
27772 code_lens_provider: Some(lsp::CodeLensOptions {
27773 resolve_provider: Some(true),
27774 }),
27775 execute_command_provider: Some(lsp::ExecuteCommandOptions {
27776 commands: vec!["_the/command".to_string()],
27777 ..lsp::ExecuteCommandOptions::default()
27778 }),
27779 ..lsp::ServerCapabilities::default()
27780 },
27781 ..FakeLspAdapter::default()
27782 },
27783 );
27784
27785 let editor = workspace
27786 .update_in(cx, |workspace, window, cx| {
27787 workspace.open_abs_path(
27788 PathBuf::from(path!("/dir/a.ts")),
27789 OpenOptions::default(),
27790 window,
27791 cx,
27792 )
27793 })
27794 .await
27795 .unwrap()
27796 .downcast::<Editor>()
27797 .unwrap();
27798 cx.executor().run_until_parked();
27799
27800 let fake_server = fake_language_servers.next().await.unwrap();
27801
27802 let buffer = editor.update(cx, |editor, cx| {
27803 editor
27804 .buffer()
27805 .read(cx)
27806 .as_singleton()
27807 .expect("have opened a single file by path")
27808 });
27809
27810 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
27811 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
27812 drop(buffer_snapshot);
27813 let actions = cx
27814 .update_window(*window, |_, window, cx| {
27815 project.code_actions(&buffer, anchor..anchor, window, cx)
27816 })
27817 .unwrap();
27818
27819 fake_server
27820 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
27821 Ok(Some(vec![
27822 lsp::CodeLens {
27823 range: lsp::Range::default(),
27824 command: Some(lsp::Command {
27825 title: "Code lens command".to_owned(),
27826 command: "_the/command".to_owned(),
27827 arguments: None,
27828 }),
27829 data: None,
27830 },
27831 lsp::CodeLens {
27832 range: lsp::Range::default(),
27833 command: Some(lsp::Command {
27834 title: "Command not in capabilities".to_owned(),
27835 command: "not in capabilities".to_owned(),
27836 arguments: None,
27837 }),
27838 data: None,
27839 },
27840 lsp::CodeLens {
27841 range: lsp::Range {
27842 start: lsp::Position {
27843 line: 1,
27844 character: 1,
27845 },
27846 end: lsp::Position {
27847 line: 1,
27848 character: 1,
27849 },
27850 },
27851 command: Some(lsp::Command {
27852 title: "Command not in range".to_owned(),
27853 command: "_the/command".to_owned(),
27854 arguments: None,
27855 }),
27856 data: None,
27857 },
27858 ]))
27859 })
27860 .next()
27861 .await;
27862
27863 let actions = actions.await.unwrap();
27864 assert_eq!(
27865 actions.len(),
27866 1,
27867 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
27868 );
27869 let action = actions[0].clone();
27870 let apply = project.update(cx, |project, cx| {
27871 project.apply_code_action(buffer.clone(), action, true, cx)
27872 });
27873
27874 // Resolving the code action does not populate its edits. In absence of
27875 // edits, we must execute the given command.
27876 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
27877 |mut lens, _| async move {
27878 let lens_command = lens.command.as_mut().expect("should have a command");
27879 assert_eq!(lens_command.title, "Code lens command");
27880 lens_command.arguments = Some(vec![json!("the-argument")]);
27881 Ok(lens)
27882 },
27883 );
27884
27885 // While executing the command, the language server sends the editor
27886 // a `workspaceEdit` request.
27887 fake_server
27888 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
27889 let fake = fake_server.clone();
27890 move |params, _| {
27891 assert_eq!(params.command, "_the/command");
27892 let fake = fake.clone();
27893 async move {
27894 fake.server
27895 .request::<lsp::request::ApplyWorkspaceEdit>(
27896 lsp::ApplyWorkspaceEditParams {
27897 label: None,
27898 edit: lsp::WorkspaceEdit {
27899 changes: Some(
27900 [(
27901 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
27902 vec![lsp::TextEdit {
27903 range: lsp::Range::new(
27904 lsp::Position::new(0, 0),
27905 lsp::Position::new(0, 0),
27906 ),
27907 new_text: "X".into(),
27908 }],
27909 )]
27910 .into_iter()
27911 .collect(),
27912 ),
27913 ..lsp::WorkspaceEdit::default()
27914 },
27915 },
27916 DEFAULT_LSP_REQUEST_TIMEOUT,
27917 )
27918 .await
27919 .into_response()
27920 .unwrap();
27921 Ok(Some(json!(null)))
27922 }
27923 }
27924 })
27925 .next()
27926 .await;
27927
27928 // Applying the code lens command returns a project transaction containing the edits
27929 // sent by the language server in its `workspaceEdit` request.
27930 let transaction = apply.await.unwrap();
27931 assert!(transaction.0.contains_key(&buffer));
27932 buffer.update(cx, |buffer, cx| {
27933 assert_eq!(buffer.text(), "Xa");
27934 buffer.undo(cx);
27935 assert_eq!(buffer.text(), "a");
27936 });
27937
27938 let actions_after_edits = cx
27939 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
27940 .unwrap()
27941 .await;
27942 assert_eq!(
27943 actions, actions_after_edits,
27944 "For the same selection, same code lens actions should be returned"
27945 );
27946
27947 let _responses =
27948 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
27949 panic!("No more code lens requests are expected");
27950 });
27951 editor.update_in(cx, |editor, window, cx| {
27952 editor.select_all(&SelectAll, window, cx);
27953 });
27954 cx.executor().run_until_parked();
27955 let new_actions = cx
27956 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
27957 .unwrap()
27958 .await;
27959 assert_eq!(
27960 actions, new_actions,
27961 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
27962 );
27963}
27964
27965#[gpui::test]
27966async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
27967 init_test(cx, |_| {});
27968
27969 let fs = FakeFs::new(cx.executor());
27970 let main_text = r#"fn main() {
27971println!("1");
27972println!("2");
27973println!("3");
27974println!("4");
27975println!("5");
27976}"#;
27977 let lib_text = "mod foo {}";
27978 fs.insert_tree(
27979 path!("/a"),
27980 json!({
27981 "lib.rs": lib_text,
27982 "main.rs": main_text,
27983 }),
27984 )
27985 .await;
27986
27987 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27988 let (multi_workspace, cx) =
27989 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27990 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27991 let worktree_id = workspace.update(cx, |workspace, cx| {
27992 workspace.project().update(cx, |project, cx| {
27993 project.worktrees(cx).next().unwrap().read(cx).id()
27994 })
27995 });
27996
27997 let expected_ranges = vec![
27998 Point::new(0, 0)..Point::new(0, 0),
27999 Point::new(1, 0)..Point::new(1, 1),
28000 Point::new(2, 0)..Point::new(2, 2),
28001 Point::new(3, 0)..Point::new(3, 3),
28002 ];
28003
28004 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28005 let editor_1 = workspace
28006 .update_in(cx, |workspace, window, cx| {
28007 workspace.open_path(
28008 (worktree_id, rel_path("main.rs")),
28009 Some(pane_1.downgrade()),
28010 true,
28011 window,
28012 cx,
28013 )
28014 })
28015 .unwrap()
28016 .await
28017 .downcast::<Editor>()
28018 .unwrap();
28019 pane_1.update(cx, |pane, cx| {
28020 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28021 open_editor.update(cx, |editor, cx| {
28022 assert_eq!(
28023 editor.display_text(cx),
28024 main_text,
28025 "Original main.rs text on initial open",
28026 );
28027 assert_eq!(
28028 editor
28029 .selections
28030 .all::<Point>(&editor.display_snapshot(cx))
28031 .into_iter()
28032 .map(|s| s.range())
28033 .collect::<Vec<_>>(),
28034 vec![Point::zero()..Point::zero()],
28035 "Default selections on initial open",
28036 );
28037 })
28038 });
28039 editor_1.update_in(cx, |editor, window, cx| {
28040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28041 s.select_ranges(expected_ranges.clone());
28042 });
28043 });
28044
28045 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
28046 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
28047 });
28048 let editor_2 = workspace
28049 .update_in(cx, |workspace, window, cx| {
28050 workspace.open_path(
28051 (worktree_id, rel_path("main.rs")),
28052 Some(pane_2.downgrade()),
28053 true,
28054 window,
28055 cx,
28056 )
28057 })
28058 .unwrap()
28059 .await
28060 .downcast::<Editor>()
28061 .unwrap();
28062 pane_2.update(cx, |pane, cx| {
28063 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28064 open_editor.update(cx, |editor, cx| {
28065 assert_eq!(
28066 editor.display_text(cx),
28067 main_text,
28068 "Original main.rs text on initial open in another panel",
28069 );
28070 assert_eq!(
28071 editor
28072 .selections
28073 .all::<Point>(&editor.display_snapshot(cx))
28074 .into_iter()
28075 .map(|s| s.range())
28076 .collect::<Vec<_>>(),
28077 vec![Point::zero()..Point::zero()],
28078 "Default selections on initial open in another panel",
28079 );
28080 })
28081 });
28082
28083 editor_2.update_in(cx, |editor, window, cx| {
28084 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
28085 });
28086
28087 let _other_editor_1 = workspace
28088 .update_in(cx, |workspace, window, cx| {
28089 workspace.open_path(
28090 (worktree_id, rel_path("lib.rs")),
28091 Some(pane_1.downgrade()),
28092 true,
28093 window,
28094 cx,
28095 )
28096 })
28097 .unwrap()
28098 .await
28099 .downcast::<Editor>()
28100 .unwrap();
28101 pane_1
28102 .update_in(cx, |pane, window, cx| {
28103 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
28104 })
28105 .await
28106 .unwrap();
28107 drop(editor_1);
28108 pane_1.update(cx, |pane, cx| {
28109 pane.active_item()
28110 .unwrap()
28111 .downcast::<Editor>()
28112 .unwrap()
28113 .update(cx, |editor, cx| {
28114 assert_eq!(
28115 editor.display_text(cx),
28116 lib_text,
28117 "Other file should be open and active",
28118 );
28119 });
28120 assert_eq!(pane.items().count(), 1, "No other editors should be open");
28121 });
28122
28123 let _other_editor_2 = workspace
28124 .update_in(cx, |workspace, window, cx| {
28125 workspace.open_path(
28126 (worktree_id, rel_path("lib.rs")),
28127 Some(pane_2.downgrade()),
28128 true,
28129 window,
28130 cx,
28131 )
28132 })
28133 .unwrap()
28134 .await
28135 .downcast::<Editor>()
28136 .unwrap();
28137 pane_2
28138 .update_in(cx, |pane, window, cx| {
28139 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
28140 })
28141 .await
28142 .unwrap();
28143 drop(editor_2);
28144 pane_2.update(cx, |pane, cx| {
28145 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28146 open_editor.update(cx, |editor, cx| {
28147 assert_eq!(
28148 editor.display_text(cx),
28149 lib_text,
28150 "Other file should be open and active in another panel too",
28151 );
28152 });
28153 assert_eq!(
28154 pane.items().count(),
28155 1,
28156 "No other editors should be open in another pane",
28157 );
28158 });
28159
28160 let _editor_1_reopened = workspace
28161 .update_in(cx, |workspace, window, cx| {
28162 workspace.open_path(
28163 (worktree_id, rel_path("main.rs")),
28164 Some(pane_1.downgrade()),
28165 true,
28166 window,
28167 cx,
28168 )
28169 })
28170 .unwrap()
28171 .await
28172 .downcast::<Editor>()
28173 .unwrap();
28174 let _editor_2_reopened = workspace
28175 .update_in(cx, |workspace, window, cx| {
28176 workspace.open_path(
28177 (worktree_id, rel_path("main.rs")),
28178 Some(pane_2.downgrade()),
28179 true,
28180 window,
28181 cx,
28182 )
28183 })
28184 .unwrap()
28185 .await
28186 .downcast::<Editor>()
28187 .unwrap();
28188 pane_1.update(cx, |pane, cx| {
28189 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28190 open_editor.update(cx, |editor, cx| {
28191 assert_eq!(
28192 editor.display_text(cx),
28193 main_text,
28194 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
28195 );
28196 assert_eq!(
28197 editor
28198 .selections
28199 .all::<Point>(&editor.display_snapshot(cx))
28200 .into_iter()
28201 .map(|s| s.range())
28202 .collect::<Vec<_>>(),
28203 expected_ranges,
28204 "Previous editor in the 1st panel had selections and should get them restored on reopen",
28205 );
28206 })
28207 });
28208 pane_2.update(cx, |pane, cx| {
28209 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28210 open_editor.update(cx, |editor, cx| {
28211 assert_eq!(
28212 editor.display_text(cx),
28213 r#"fn main() {
28214⋯rintln!("1");
28215⋯intln!("2");
28216⋯ntln!("3");
28217println!("4");
28218println!("5");
28219}"#,
28220 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
28221 );
28222 assert_eq!(
28223 editor
28224 .selections
28225 .all::<Point>(&editor.display_snapshot(cx))
28226 .into_iter()
28227 .map(|s| s.range())
28228 .collect::<Vec<_>>(),
28229 vec![Point::zero()..Point::zero()],
28230 "Previous editor in the 2nd pane had no selections changed hence should restore none",
28231 );
28232 })
28233 });
28234}
28235
28236#[gpui::test]
28237async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
28238 init_test(cx, |_| {});
28239
28240 let fs = FakeFs::new(cx.executor());
28241 let main_text = r#"fn main() {
28242println!("1");
28243println!("2");
28244println!("3");
28245println!("4");
28246println!("5");
28247}"#;
28248 let lib_text = "mod foo {}";
28249 fs.insert_tree(
28250 path!("/a"),
28251 json!({
28252 "lib.rs": lib_text,
28253 "main.rs": main_text,
28254 }),
28255 )
28256 .await;
28257
28258 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28259 let (multi_workspace, cx) =
28260 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28261 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28262 let worktree_id = workspace.update(cx, |workspace, cx| {
28263 workspace.project().update(cx, |project, cx| {
28264 project.worktrees(cx).next().unwrap().read(cx).id()
28265 })
28266 });
28267
28268 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28269 let editor = workspace
28270 .update_in(cx, |workspace, window, cx| {
28271 workspace.open_path(
28272 (worktree_id, rel_path("main.rs")),
28273 Some(pane.downgrade()),
28274 true,
28275 window,
28276 cx,
28277 )
28278 })
28279 .unwrap()
28280 .await
28281 .downcast::<Editor>()
28282 .unwrap();
28283 pane.update(cx, |pane, cx| {
28284 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28285 open_editor.update(cx, |editor, cx| {
28286 assert_eq!(
28287 editor.display_text(cx),
28288 main_text,
28289 "Original main.rs text on initial open",
28290 );
28291 })
28292 });
28293 editor.update_in(cx, |editor, window, cx| {
28294 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
28295 });
28296
28297 cx.update_global(|store: &mut SettingsStore, cx| {
28298 store.update_user_settings(cx, |s| {
28299 s.workspace.restore_on_file_reopen = Some(false);
28300 });
28301 });
28302 editor.update_in(cx, |editor, window, cx| {
28303 editor.fold_ranges(
28304 vec![
28305 Point::new(1, 0)..Point::new(1, 1),
28306 Point::new(2, 0)..Point::new(2, 2),
28307 Point::new(3, 0)..Point::new(3, 3),
28308 ],
28309 false,
28310 window,
28311 cx,
28312 );
28313 });
28314 pane.update_in(cx, |pane, window, cx| {
28315 pane.close_all_items(&CloseAllItems::default(), window, cx)
28316 })
28317 .await
28318 .unwrap();
28319 pane.update(cx, |pane, _| {
28320 assert!(pane.active_item().is_none());
28321 });
28322 cx.update_global(|store: &mut SettingsStore, cx| {
28323 store.update_user_settings(cx, |s| {
28324 s.workspace.restore_on_file_reopen = Some(true);
28325 });
28326 });
28327
28328 let _editor_reopened = workspace
28329 .update_in(cx, |workspace, window, cx| {
28330 workspace.open_path(
28331 (worktree_id, rel_path("main.rs")),
28332 Some(pane.downgrade()),
28333 true,
28334 window,
28335 cx,
28336 )
28337 })
28338 .unwrap()
28339 .await
28340 .downcast::<Editor>()
28341 .unwrap();
28342 pane.update(cx, |pane, cx| {
28343 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28344 open_editor.update(cx, |editor, cx| {
28345 assert_eq!(
28346 editor.display_text(cx),
28347 main_text,
28348 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
28349 );
28350 })
28351 });
28352}
28353
28354#[gpui::test]
28355async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
28356 struct EmptyModalView {
28357 focus_handle: gpui::FocusHandle,
28358 }
28359 impl EventEmitter<DismissEvent> for EmptyModalView {}
28360 impl Render for EmptyModalView {
28361 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
28362 div()
28363 }
28364 }
28365 impl Focusable for EmptyModalView {
28366 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
28367 self.focus_handle.clone()
28368 }
28369 }
28370 impl workspace::ModalView for EmptyModalView {}
28371 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
28372 EmptyModalView {
28373 focus_handle: cx.focus_handle(),
28374 }
28375 }
28376
28377 init_test(cx, |_| {});
28378
28379 let fs = FakeFs::new(cx.executor());
28380 let project = Project::test(fs, [], cx).await;
28381 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28382 let workspace = window
28383 .read_with(cx, |mw, _| mw.workspace().clone())
28384 .unwrap();
28385 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
28386 let cx = &mut VisualTestContext::from_window(*window, cx);
28387 let editor = cx.new_window_entity(|window, cx| {
28388 Editor::new(
28389 EditorMode::full(),
28390 buffer,
28391 Some(project.clone()),
28392 window,
28393 cx,
28394 )
28395 });
28396 workspace.update_in(cx, |workspace, window, cx| {
28397 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
28398 });
28399
28400 editor.update_in(cx, |editor, window, cx| {
28401 editor.open_context_menu(&OpenContextMenu, window, cx);
28402 assert!(editor.mouse_context_menu.is_some());
28403 });
28404 workspace.update_in(cx, |workspace, window, cx| {
28405 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
28406 });
28407
28408 cx.read(|cx| {
28409 assert!(editor.read(cx).mouse_context_menu.is_none());
28410 });
28411}
28412
28413fn set_linked_edit_ranges(
28414 opening: (Point, Point),
28415 closing: (Point, Point),
28416 editor: &mut Editor,
28417 cx: &mut Context<Editor>,
28418) {
28419 let Some((buffer, _)) = editor
28420 .buffer
28421 .read(cx)
28422 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
28423 else {
28424 panic!("Failed to get buffer for selection position");
28425 };
28426 let buffer = buffer.read(cx);
28427 let buffer_id = buffer.remote_id();
28428 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
28429 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
28430 let mut linked_ranges = HashMap::default();
28431 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
28432 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
28433}
28434
28435#[gpui::test]
28436async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
28437 init_test(cx, |_| {});
28438
28439 let fs = FakeFs::new(cx.executor());
28440 fs.insert_file(path!("/file.html"), Default::default())
28441 .await;
28442
28443 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
28444
28445 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28446 let html_language = Arc::new(Language::new(
28447 LanguageConfig {
28448 name: "HTML".into(),
28449 matcher: LanguageMatcher {
28450 path_suffixes: vec!["html".to_string()],
28451 ..LanguageMatcher::default()
28452 },
28453 brackets: BracketPairConfig {
28454 pairs: vec![BracketPair {
28455 start: "<".into(),
28456 end: ">".into(),
28457 close: true,
28458 ..Default::default()
28459 }],
28460 ..Default::default()
28461 },
28462 ..Default::default()
28463 },
28464 Some(tree_sitter_html::LANGUAGE.into()),
28465 ));
28466 language_registry.add(html_language);
28467 let mut fake_servers = language_registry.register_fake_lsp(
28468 "HTML",
28469 FakeLspAdapter {
28470 capabilities: lsp::ServerCapabilities {
28471 completion_provider: Some(lsp::CompletionOptions {
28472 resolve_provider: Some(true),
28473 ..Default::default()
28474 }),
28475 ..Default::default()
28476 },
28477 ..Default::default()
28478 },
28479 );
28480
28481 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28482 let workspace = window
28483 .read_with(cx, |mw, _| mw.workspace().clone())
28484 .unwrap();
28485 let cx = &mut VisualTestContext::from_window(*window, cx);
28486
28487 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
28488 workspace.project().update(cx, |project, cx| {
28489 project.worktrees(cx).next().unwrap().read(cx).id()
28490 })
28491 });
28492
28493 project
28494 .update(cx, |project, cx| {
28495 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
28496 })
28497 .await
28498 .unwrap();
28499 let editor = workspace
28500 .update_in(cx, |workspace, window, cx| {
28501 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
28502 })
28503 .await
28504 .unwrap()
28505 .downcast::<Editor>()
28506 .unwrap();
28507
28508 let fake_server = fake_servers.next().await.unwrap();
28509 cx.run_until_parked();
28510 editor.update_in(cx, |editor, window, cx| {
28511 editor.set_text("<ad></ad>", window, cx);
28512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
28513 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
28514 });
28515 set_linked_edit_ranges(
28516 (Point::new(0, 1), Point::new(0, 3)),
28517 (Point::new(0, 6), Point::new(0, 8)),
28518 editor,
28519 cx,
28520 );
28521 });
28522 let mut completion_handle =
28523 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
28524 Ok(Some(lsp::CompletionResponse::Array(vec![
28525 lsp::CompletionItem {
28526 label: "head".to_string(),
28527 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
28528 lsp::InsertReplaceEdit {
28529 new_text: "head".to_string(),
28530 insert: lsp::Range::new(
28531 lsp::Position::new(0, 1),
28532 lsp::Position::new(0, 3),
28533 ),
28534 replace: lsp::Range::new(
28535 lsp::Position::new(0, 1),
28536 lsp::Position::new(0, 3),
28537 ),
28538 },
28539 )),
28540 ..Default::default()
28541 },
28542 ])))
28543 });
28544 editor.update_in(cx, |editor, window, cx| {
28545 editor.show_completions(&ShowCompletions, window, cx);
28546 });
28547 cx.run_until_parked();
28548 completion_handle.next().await.unwrap();
28549 editor.update(cx, |editor, _| {
28550 assert!(
28551 editor.context_menu_visible(),
28552 "Completion menu should be visible"
28553 );
28554 });
28555 editor.update_in(cx, |editor, window, cx| {
28556 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
28557 });
28558 cx.executor().run_until_parked();
28559 editor.update(cx, |editor, cx| {
28560 assert_eq!(editor.text(cx), "<head></head>");
28561 });
28562}
28563
28564#[gpui::test]
28565async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
28566 init_test(cx, |_| {});
28567
28568 let mut cx = EditorTestContext::new(cx).await;
28569 let language = Arc::new(Language::new(
28570 LanguageConfig {
28571 name: "TSX".into(),
28572 matcher: LanguageMatcher {
28573 path_suffixes: vec!["tsx".to_string()],
28574 ..LanguageMatcher::default()
28575 },
28576 brackets: BracketPairConfig {
28577 pairs: vec![BracketPair {
28578 start: "<".into(),
28579 end: ">".into(),
28580 close: true,
28581 ..Default::default()
28582 }],
28583 ..Default::default()
28584 },
28585 linked_edit_characters: HashSet::from_iter(['.']),
28586 ..Default::default()
28587 },
28588 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
28589 ));
28590 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28591
28592 // Test typing > does not extend linked pair
28593 cx.set_state("<divˇ<div></div>");
28594 cx.update_editor(|editor, _, cx| {
28595 set_linked_edit_ranges(
28596 (Point::new(0, 1), Point::new(0, 4)),
28597 (Point::new(0, 11), Point::new(0, 14)),
28598 editor,
28599 cx,
28600 );
28601 });
28602 cx.update_editor(|editor, window, cx| {
28603 editor.handle_input(">", window, cx);
28604 });
28605 cx.assert_editor_state("<div>ˇ<div></div>");
28606
28607 // Test typing . do extend linked pair
28608 cx.set_state("<Animatedˇ></Animated>");
28609 cx.update_editor(|editor, _, cx| {
28610 set_linked_edit_ranges(
28611 (Point::new(0, 1), Point::new(0, 9)),
28612 (Point::new(0, 12), Point::new(0, 20)),
28613 editor,
28614 cx,
28615 );
28616 });
28617 cx.update_editor(|editor, window, cx| {
28618 editor.handle_input(".", window, cx);
28619 });
28620 cx.assert_editor_state("<Animated.ˇ></Animated.>");
28621 cx.update_editor(|editor, _, cx| {
28622 set_linked_edit_ranges(
28623 (Point::new(0, 1), Point::new(0, 10)),
28624 (Point::new(0, 13), Point::new(0, 21)),
28625 editor,
28626 cx,
28627 );
28628 });
28629 cx.update_editor(|editor, window, cx| {
28630 editor.handle_input("V", window, cx);
28631 });
28632 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
28633}
28634
28635#[gpui::test]
28636async fn test_linked_edits_on_typing_dot_without_language_override(cx: &mut TestAppContext) {
28637 init_test(cx, |_| {});
28638
28639 let mut cx = EditorTestContext::new(cx).await;
28640 let language = Arc::new(Language::new(
28641 LanguageConfig {
28642 name: "HTML".into(),
28643 matcher: LanguageMatcher {
28644 path_suffixes: vec!["html".to_string()],
28645 ..LanguageMatcher::default()
28646 },
28647 brackets: BracketPairConfig {
28648 pairs: vec![BracketPair {
28649 start: "<".into(),
28650 end: ">".into(),
28651 close: true,
28652 ..Default::default()
28653 }],
28654 ..Default::default()
28655 },
28656 ..Default::default()
28657 },
28658 Some(tree_sitter_html::LANGUAGE.into()),
28659 ));
28660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28661
28662 cx.set_state("<Tableˇ></Table>");
28663 cx.update_editor(|editor, _, cx| {
28664 set_linked_edit_ranges(
28665 (Point::new(0, 1), Point::new(0, 6)),
28666 (Point::new(0, 9), Point::new(0, 14)),
28667 editor,
28668 cx,
28669 );
28670 });
28671 cx.update_editor(|editor, window, cx| {
28672 editor.handle_input(".", window, cx);
28673 });
28674 cx.assert_editor_state("<Table.ˇ></Table.>");
28675}
28676
28677#[gpui::test]
28678async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
28679 init_test(cx, |_| {});
28680
28681 let fs = FakeFs::new(cx.executor());
28682 fs.insert_tree(
28683 path!("/root"),
28684 json!({
28685 "a": {
28686 "main.rs": "fn main() {}",
28687 },
28688 "foo": {
28689 "bar": {
28690 "external_file.rs": "pub mod external {}",
28691 }
28692 }
28693 }),
28694 )
28695 .await;
28696
28697 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
28698 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28699 language_registry.add(rust_lang());
28700 let _fake_servers = language_registry.register_fake_lsp(
28701 "Rust",
28702 FakeLspAdapter {
28703 ..FakeLspAdapter::default()
28704 },
28705 );
28706 let (multi_workspace, cx) =
28707 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28708 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28709 let worktree_id = workspace.update(cx, |workspace, cx| {
28710 workspace.project().update(cx, |project, cx| {
28711 project.worktrees(cx).next().unwrap().read(cx).id()
28712 })
28713 });
28714
28715 let assert_language_servers_count =
28716 |expected: usize, context: &str, cx: &mut VisualTestContext| {
28717 project.update(cx, |project, cx| {
28718 let current = project
28719 .lsp_store()
28720 .read(cx)
28721 .as_local()
28722 .unwrap()
28723 .language_servers
28724 .len();
28725 assert_eq!(expected, current, "{context}");
28726 });
28727 };
28728
28729 assert_language_servers_count(
28730 0,
28731 "No servers should be running before any file is open",
28732 cx,
28733 );
28734 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
28735 let main_editor = workspace
28736 .update_in(cx, |workspace, window, cx| {
28737 workspace.open_path(
28738 (worktree_id, rel_path("main.rs")),
28739 Some(pane.downgrade()),
28740 true,
28741 window,
28742 cx,
28743 )
28744 })
28745 .unwrap()
28746 .await
28747 .downcast::<Editor>()
28748 .unwrap();
28749 pane.update(cx, |pane, cx| {
28750 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28751 open_editor.update(cx, |editor, cx| {
28752 assert_eq!(
28753 editor.display_text(cx),
28754 "fn main() {}",
28755 "Original main.rs text on initial open",
28756 );
28757 });
28758 assert_eq!(open_editor, main_editor);
28759 });
28760 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
28761
28762 let external_editor = workspace
28763 .update_in(cx, |workspace, window, cx| {
28764 workspace.open_abs_path(
28765 PathBuf::from("/root/foo/bar/external_file.rs"),
28766 OpenOptions::default(),
28767 window,
28768 cx,
28769 )
28770 })
28771 .await
28772 .expect("opening external file")
28773 .downcast::<Editor>()
28774 .expect("downcasted external file's open element to editor");
28775 pane.update(cx, |pane, cx| {
28776 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28777 open_editor.update(cx, |editor, cx| {
28778 assert_eq!(
28779 editor.display_text(cx),
28780 "pub mod external {}",
28781 "External file is open now",
28782 );
28783 });
28784 assert_eq!(open_editor, external_editor);
28785 });
28786 assert_language_servers_count(
28787 1,
28788 "Second, external, *.rs file should join the existing server",
28789 cx,
28790 );
28791
28792 pane.update_in(cx, |pane, window, cx| {
28793 pane.close_active_item(&CloseActiveItem::default(), window, cx)
28794 })
28795 .await
28796 .unwrap();
28797 pane.update_in(cx, |pane, window, cx| {
28798 pane.navigate_backward(&Default::default(), window, cx);
28799 });
28800 cx.run_until_parked();
28801 pane.update(cx, |pane, cx| {
28802 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
28803 open_editor.update(cx, |editor, cx| {
28804 assert_eq!(
28805 editor.display_text(cx),
28806 "pub mod external {}",
28807 "External file is open now",
28808 );
28809 });
28810 });
28811 assert_language_servers_count(
28812 1,
28813 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
28814 cx,
28815 );
28816
28817 cx.update(|_, cx| {
28818 workspace::reload(cx);
28819 });
28820 assert_language_servers_count(
28821 1,
28822 "After reloading the worktree with local and external files opened, only one project should be started",
28823 cx,
28824 );
28825}
28826
28827#[gpui::test]
28828async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
28829 init_test(cx, |_| {});
28830
28831 let mut cx = EditorTestContext::new(cx).await;
28832 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
28833 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28834
28835 // test cursor move to start of each line on tab
28836 // for `if`, `elif`, `else`, `while`, `with` and `for`
28837 cx.set_state(indoc! {"
28838 def main():
28839 ˇ for item in items:
28840 ˇ while item.active:
28841 ˇ if item.value > 10:
28842 ˇ continue
28843 ˇ elif item.value < 0:
28844 ˇ break
28845 ˇ else:
28846 ˇ with item.context() as ctx:
28847 ˇ yield count
28848 ˇ else:
28849 ˇ log('while else')
28850 ˇ else:
28851 ˇ log('for else')
28852 "});
28853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28854 cx.wait_for_autoindent_applied().await;
28855 cx.assert_editor_state(indoc! {"
28856 def main():
28857 ˇfor item in items:
28858 ˇwhile item.active:
28859 ˇif item.value > 10:
28860 ˇcontinue
28861 ˇelif item.value < 0:
28862 ˇbreak
28863 ˇelse:
28864 ˇwith item.context() as ctx:
28865 ˇyield count
28866 ˇelse:
28867 ˇlog('while else')
28868 ˇelse:
28869 ˇlog('for else')
28870 "});
28871 // test relative indent is preserved when tab
28872 // for `if`, `elif`, `else`, `while`, `with` and `for`
28873 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28874 cx.wait_for_autoindent_applied().await;
28875 cx.assert_editor_state(indoc! {"
28876 def main():
28877 ˇfor item in items:
28878 ˇwhile item.active:
28879 ˇif item.value > 10:
28880 ˇcontinue
28881 ˇelif item.value < 0:
28882 ˇbreak
28883 ˇelse:
28884 ˇwith item.context() as ctx:
28885 ˇyield count
28886 ˇelse:
28887 ˇlog('while else')
28888 ˇelse:
28889 ˇlog('for else')
28890 "});
28891
28892 // test cursor move to start of each line on tab
28893 // for `try`, `except`, `else`, `finally`, `match` and `def`
28894 cx.set_state(indoc! {"
28895 def main():
28896 ˇ try:
28897 ˇ fetch()
28898 ˇ except ValueError:
28899 ˇ handle_error()
28900 ˇ else:
28901 ˇ match value:
28902 ˇ case _:
28903 ˇ finally:
28904 ˇ def status():
28905 ˇ return 0
28906 "});
28907 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28908 cx.wait_for_autoindent_applied().await;
28909 cx.assert_editor_state(indoc! {"
28910 def main():
28911 ˇtry:
28912 ˇfetch()
28913 ˇexcept ValueError:
28914 ˇhandle_error()
28915 ˇelse:
28916 ˇmatch value:
28917 ˇcase _:
28918 ˇfinally:
28919 ˇdef status():
28920 ˇreturn 0
28921 "});
28922 // test relative indent is preserved when tab
28923 // for `try`, `except`, `else`, `finally`, `match` and `def`
28924 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28925 cx.wait_for_autoindent_applied().await;
28926 cx.assert_editor_state(indoc! {"
28927 def main():
28928 ˇtry:
28929 ˇfetch()
28930 ˇexcept ValueError:
28931 ˇhandle_error()
28932 ˇelse:
28933 ˇmatch value:
28934 ˇcase _:
28935 ˇfinally:
28936 ˇdef status():
28937 ˇreturn 0
28938 "});
28939}
28940
28941#[gpui::test]
28942async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
28943 init_test(cx, |_| {});
28944
28945 let mut cx = EditorTestContext::new(cx).await;
28946 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
28947 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28948
28949 // test `else` auto outdents when typed inside `if` block
28950 cx.set_state(indoc! {"
28951 def main():
28952 if i == 2:
28953 return
28954 ˇ
28955 "});
28956 cx.update_editor(|editor, window, cx| {
28957 editor.handle_input("else:", window, cx);
28958 });
28959 cx.wait_for_autoindent_applied().await;
28960 cx.assert_editor_state(indoc! {"
28961 def main():
28962 if i == 2:
28963 return
28964 else:ˇ
28965 "});
28966
28967 // test `except` auto outdents when typed inside `try` block
28968 cx.set_state(indoc! {"
28969 def main():
28970 try:
28971 i = 2
28972 ˇ
28973 "});
28974 cx.update_editor(|editor, window, cx| {
28975 editor.handle_input("except:", window, cx);
28976 });
28977 cx.wait_for_autoindent_applied().await;
28978 cx.assert_editor_state(indoc! {"
28979 def main():
28980 try:
28981 i = 2
28982 except:ˇ
28983 "});
28984
28985 // test `else` auto outdents when typed inside `except` block
28986 cx.set_state(indoc! {"
28987 def main():
28988 try:
28989 i = 2
28990 except:
28991 j = 2
28992 ˇ
28993 "});
28994 cx.update_editor(|editor, window, cx| {
28995 editor.handle_input("else:", window, cx);
28996 });
28997 cx.wait_for_autoindent_applied().await;
28998 cx.assert_editor_state(indoc! {"
28999 def main():
29000 try:
29001 i = 2
29002 except:
29003 j = 2
29004 else:ˇ
29005 "});
29006
29007 // test `finally` auto outdents when typed inside `else` block
29008 cx.set_state(indoc! {"
29009 def main():
29010 try:
29011 i = 2
29012 except:
29013 j = 2
29014 else:
29015 k = 2
29016 ˇ
29017 "});
29018 cx.update_editor(|editor, window, cx| {
29019 editor.handle_input("finally:", window, cx);
29020 });
29021 cx.wait_for_autoindent_applied().await;
29022 cx.assert_editor_state(indoc! {"
29023 def main():
29024 try:
29025 i = 2
29026 except:
29027 j = 2
29028 else:
29029 k = 2
29030 finally:ˇ
29031 "});
29032
29033 // test `else` does not outdents when typed inside `except` block right after for block
29034 cx.set_state(indoc! {"
29035 def main():
29036 try:
29037 i = 2
29038 except:
29039 for i in range(n):
29040 pass
29041 ˇ
29042 "});
29043 cx.update_editor(|editor, window, cx| {
29044 editor.handle_input("else:", window, cx);
29045 });
29046 cx.wait_for_autoindent_applied().await;
29047 cx.assert_editor_state(indoc! {"
29048 def main():
29049 try:
29050 i = 2
29051 except:
29052 for i in range(n):
29053 pass
29054 else:ˇ
29055 "});
29056
29057 // test `finally` auto outdents when typed inside `else` block right after for block
29058 cx.set_state(indoc! {"
29059 def main():
29060 try:
29061 i = 2
29062 except:
29063 j = 2
29064 else:
29065 for i in range(n):
29066 pass
29067 ˇ
29068 "});
29069 cx.update_editor(|editor, window, cx| {
29070 editor.handle_input("finally:", window, cx);
29071 });
29072 cx.wait_for_autoindent_applied().await;
29073 cx.assert_editor_state(indoc! {"
29074 def main():
29075 try:
29076 i = 2
29077 except:
29078 j = 2
29079 else:
29080 for i in range(n):
29081 pass
29082 finally:ˇ
29083 "});
29084
29085 // test `except` outdents to inner "try" block
29086 cx.set_state(indoc! {"
29087 def main():
29088 try:
29089 i = 2
29090 if i == 2:
29091 try:
29092 i = 3
29093 ˇ
29094 "});
29095 cx.update_editor(|editor, window, cx| {
29096 editor.handle_input("except:", window, cx);
29097 });
29098 cx.wait_for_autoindent_applied().await;
29099 cx.assert_editor_state(indoc! {"
29100 def main():
29101 try:
29102 i = 2
29103 if i == 2:
29104 try:
29105 i = 3
29106 except:ˇ
29107 "});
29108
29109 // test `except` outdents to outer "try" block
29110 cx.set_state(indoc! {"
29111 def main():
29112 try:
29113 i = 2
29114 if i == 2:
29115 try:
29116 i = 3
29117 ˇ
29118 "});
29119 cx.update_editor(|editor, window, cx| {
29120 editor.handle_input("except:", window, cx);
29121 });
29122 cx.wait_for_autoindent_applied().await;
29123 cx.assert_editor_state(indoc! {"
29124 def main():
29125 try:
29126 i = 2
29127 if i == 2:
29128 try:
29129 i = 3
29130 except:ˇ
29131 "});
29132
29133 // test `else` stays at correct indent when typed after `for` block
29134 cx.set_state(indoc! {"
29135 def main():
29136 for i in range(10):
29137 if i == 3:
29138 break
29139 ˇ
29140 "});
29141 cx.update_editor(|editor, window, cx| {
29142 editor.handle_input("else:", window, cx);
29143 });
29144 cx.wait_for_autoindent_applied().await;
29145 cx.assert_editor_state(indoc! {"
29146 def main():
29147 for i in range(10):
29148 if i == 3:
29149 break
29150 else:ˇ
29151 "});
29152
29153 // test does not outdent on typing after line with square brackets
29154 cx.set_state(indoc! {"
29155 def f() -> list[str]:
29156 ˇ
29157 "});
29158 cx.update_editor(|editor, window, cx| {
29159 editor.handle_input("a", window, cx);
29160 });
29161 cx.wait_for_autoindent_applied().await;
29162 cx.assert_editor_state(indoc! {"
29163 def f() -> list[str]:
29164 aˇ
29165 "});
29166
29167 // test does not outdent on typing : after case keyword
29168 cx.set_state(indoc! {"
29169 match 1:
29170 caseˇ
29171 "});
29172 cx.update_editor(|editor, window, cx| {
29173 editor.handle_input(":", window, cx);
29174 });
29175 cx.wait_for_autoindent_applied().await;
29176 cx.assert_editor_state(indoc! {"
29177 match 1:
29178 case:ˇ
29179 "});
29180}
29181
29182#[gpui::test]
29183async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
29184 init_test(cx, |_| {});
29185 update_test_language_settings(cx, &|settings| {
29186 settings.defaults.extend_comment_on_newline = Some(false);
29187 });
29188 let mut cx = EditorTestContext::new(cx).await;
29189 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
29190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29191
29192 // test correct indent after newline on comment
29193 cx.set_state(indoc! {"
29194 # COMMENT:ˇ
29195 "});
29196 cx.update_editor(|editor, window, cx| {
29197 editor.newline(&Newline, window, cx);
29198 });
29199 cx.wait_for_autoindent_applied().await;
29200 cx.assert_editor_state(indoc! {"
29201 # COMMENT:
29202 ˇ
29203 "});
29204
29205 // test correct indent after newline in brackets
29206 cx.set_state(indoc! {"
29207 {ˇ}
29208 "});
29209 cx.update_editor(|editor, window, cx| {
29210 editor.newline(&Newline, window, cx);
29211 });
29212 cx.wait_for_autoindent_applied().await;
29213 cx.assert_editor_state(indoc! {"
29214 {
29215 ˇ
29216 }
29217 "});
29218
29219 cx.set_state(indoc! {"
29220 (ˇ)
29221 "});
29222 cx.update_editor(|editor, window, cx| {
29223 editor.newline(&Newline, window, cx);
29224 });
29225 cx.run_until_parked();
29226 cx.assert_editor_state(indoc! {"
29227 (
29228 ˇ
29229 )
29230 "});
29231
29232 // do not indent after empty lists or dictionaries
29233 cx.set_state(indoc! {"
29234 a = []ˇ
29235 "});
29236 cx.update_editor(|editor, window, cx| {
29237 editor.newline(&Newline, window, cx);
29238 });
29239 cx.run_until_parked();
29240 cx.assert_editor_state(indoc! {"
29241 a = []
29242 ˇ
29243 "});
29244}
29245
29246#[gpui::test]
29247async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
29248 init_test(cx, |_| {});
29249
29250 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
29251 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
29252 language_registry.add(markdown_lang());
29253 language_registry.add(python_lang);
29254
29255 let mut cx = EditorTestContext::new(cx).await;
29256 cx.update_buffer(|buffer, cx| {
29257 buffer.set_language_registry(language_registry);
29258 buffer.set_language(Some(markdown_lang()), cx);
29259 });
29260
29261 // Test that `else:` correctly outdents to match `if:` inside the Python code block
29262 cx.set_state(indoc! {"
29263 # Heading
29264
29265 ```python
29266 def main():
29267 if condition:
29268 pass
29269 ˇ
29270 ```
29271 "});
29272 cx.update_editor(|editor, window, cx| {
29273 editor.handle_input("else:", window, cx);
29274 });
29275 cx.run_until_parked();
29276 cx.assert_editor_state(indoc! {"
29277 # Heading
29278
29279 ```python
29280 def main():
29281 if condition:
29282 pass
29283 else:ˇ
29284 ```
29285 "});
29286}
29287
29288#[gpui::test]
29289async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
29290 init_test(cx, |_| {});
29291
29292 let mut cx = EditorTestContext::new(cx).await;
29293 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29294 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29295
29296 // test cursor move to start of each line on tab
29297 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
29298 cx.set_state(indoc! {"
29299 function main() {
29300 ˇ for item in $items; do
29301 ˇ while [ -n \"$item\" ]; do
29302 ˇ if [ \"$value\" -gt 10 ]; then
29303 ˇ continue
29304 ˇ elif [ \"$value\" -lt 0 ]; then
29305 ˇ break
29306 ˇ else
29307 ˇ echo \"$item\"
29308 ˇ fi
29309 ˇ done
29310 ˇ done
29311 ˇ}
29312 "});
29313 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29314 cx.wait_for_autoindent_applied().await;
29315 cx.assert_editor_state(indoc! {"
29316 function main() {
29317 ˇfor item in $items; do
29318 ˇwhile [ -n \"$item\" ]; do
29319 ˇif [ \"$value\" -gt 10 ]; then
29320 ˇcontinue
29321 ˇelif [ \"$value\" -lt 0 ]; then
29322 ˇbreak
29323 ˇelse
29324 ˇecho \"$item\"
29325 ˇfi
29326 ˇdone
29327 ˇdone
29328 ˇ}
29329 "});
29330 // test relative indent is preserved when tab
29331 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29332 cx.wait_for_autoindent_applied().await;
29333 cx.assert_editor_state(indoc! {"
29334 function main() {
29335 ˇfor item in $items; do
29336 ˇwhile [ -n \"$item\" ]; do
29337 ˇif [ \"$value\" -gt 10 ]; then
29338 ˇcontinue
29339 ˇelif [ \"$value\" -lt 0 ]; then
29340 ˇbreak
29341 ˇelse
29342 ˇecho \"$item\"
29343 ˇfi
29344 ˇdone
29345 ˇdone
29346 ˇ}
29347 "});
29348
29349 // test cursor move to start of each line on tab
29350 // for `case` statement with patterns
29351 cx.set_state(indoc! {"
29352 function handle() {
29353 ˇ case \"$1\" in
29354 ˇ start)
29355 ˇ echo \"a\"
29356 ˇ ;;
29357 ˇ stop)
29358 ˇ echo \"b\"
29359 ˇ ;;
29360 ˇ *)
29361 ˇ echo \"c\"
29362 ˇ ;;
29363 ˇ esac
29364 ˇ}
29365 "});
29366 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29367 cx.wait_for_autoindent_applied().await;
29368 cx.assert_editor_state(indoc! {"
29369 function handle() {
29370 ˇcase \"$1\" in
29371 ˇstart)
29372 ˇecho \"a\"
29373 ˇ;;
29374 ˇstop)
29375 ˇecho \"b\"
29376 ˇ;;
29377 ˇ*)
29378 ˇecho \"c\"
29379 ˇ;;
29380 ˇesac
29381 ˇ}
29382 "});
29383}
29384
29385#[gpui::test]
29386async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
29387 init_test(cx, |_| {});
29388
29389 let mut cx = EditorTestContext::new(cx).await;
29390 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29391 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29392
29393 // test indents on comment insert
29394 cx.set_state(indoc! {"
29395 function main() {
29396 ˇ for item in $items; do
29397 ˇ while [ -n \"$item\" ]; do
29398 ˇ if [ \"$value\" -gt 10 ]; then
29399 ˇ continue
29400 ˇ elif [ \"$value\" -lt 0 ]; then
29401 ˇ break
29402 ˇ else
29403 ˇ echo \"$item\"
29404 ˇ fi
29405 ˇ done
29406 ˇ done
29407 ˇ}
29408 "});
29409 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
29410 cx.wait_for_autoindent_applied().await;
29411 cx.assert_editor_state(indoc! {"
29412 function main() {
29413 #ˇ for item in $items; do
29414 #ˇ while [ -n \"$item\" ]; do
29415 #ˇ if [ \"$value\" -gt 10 ]; then
29416 #ˇ continue
29417 #ˇ elif [ \"$value\" -lt 0 ]; then
29418 #ˇ break
29419 #ˇ else
29420 #ˇ echo \"$item\"
29421 #ˇ fi
29422 #ˇ done
29423 #ˇ done
29424 #ˇ}
29425 "});
29426}
29427
29428#[gpui::test]
29429async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
29430 init_test(cx, |_| {});
29431
29432 let mut cx = EditorTestContext::new(cx).await;
29433 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29434 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29435
29436 // test `else` auto outdents when typed inside `if` block
29437 cx.set_state(indoc! {"
29438 if [ \"$1\" = \"test\" ]; then
29439 echo \"foo bar\"
29440 ˇ
29441 "});
29442 cx.update_editor(|editor, window, cx| {
29443 editor.handle_input("else", window, cx);
29444 });
29445 cx.wait_for_autoindent_applied().await;
29446 cx.assert_editor_state(indoc! {"
29447 if [ \"$1\" = \"test\" ]; then
29448 echo \"foo bar\"
29449 elseˇ
29450 "});
29451
29452 // test `elif` auto outdents when typed inside `if` block
29453 cx.set_state(indoc! {"
29454 if [ \"$1\" = \"test\" ]; then
29455 echo \"foo bar\"
29456 ˇ
29457 "});
29458 cx.update_editor(|editor, window, cx| {
29459 editor.handle_input("elif", window, cx);
29460 });
29461 cx.wait_for_autoindent_applied().await;
29462 cx.assert_editor_state(indoc! {"
29463 if [ \"$1\" = \"test\" ]; then
29464 echo \"foo bar\"
29465 elifˇ
29466 "});
29467
29468 // test `fi` auto outdents when typed inside `else` block
29469 cx.set_state(indoc! {"
29470 if [ \"$1\" = \"test\" ]; then
29471 echo \"foo bar\"
29472 else
29473 echo \"bar baz\"
29474 ˇ
29475 "});
29476 cx.update_editor(|editor, window, cx| {
29477 editor.handle_input("fi", window, cx);
29478 });
29479 cx.wait_for_autoindent_applied().await;
29480 cx.assert_editor_state(indoc! {"
29481 if [ \"$1\" = \"test\" ]; then
29482 echo \"foo bar\"
29483 else
29484 echo \"bar baz\"
29485 fiˇ
29486 "});
29487
29488 // test `done` auto outdents when typed inside `while` block
29489 cx.set_state(indoc! {"
29490 while read line; do
29491 echo \"$line\"
29492 ˇ
29493 "});
29494 cx.update_editor(|editor, window, cx| {
29495 editor.handle_input("done", window, cx);
29496 });
29497 cx.wait_for_autoindent_applied().await;
29498 cx.assert_editor_state(indoc! {"
29499 while read line; do
29500 echo \"$line\"
29501 doneˇ
29502 "});
29503
29504 // test `done` auto outdents when typed inside `for` block
29505 cx.set_state(indoc! {"
29506 for file in *.txt; do
29507 cat \"$file\"
29508 ˇ
29509 "});
29510 cx.update_editor(|editor, window, cx| {
29511 editor.handle_input("done", window, cx);
29512 });
29513 cx.wait_for_autoindent_applied().await;
29514 cx.assert_editor_state(indoc! {"
29515 for file in *.txt; do
29516 cat \"$file\"
29517 doneˇ
29518 "});
29519
29520 // test `esac` auto outdents when typed inside `case` block
29521 cx.set_state(indoc! {"
29522 case \"$1\" in
29523 start)
29524 echo \"foo bar\"
29525 ;;
29526 stop)
29527 echo \"bar baz\"
29528 ;;
29529 ˇ
29530 "});
29531 cx.update_editor(|editor, window, cx| {
29532 editor.handle_input("esac", window, cx);
29533 });
29534 cx.wait_for_autoindent_applied().await;
29535 cx.assert_editor_state(indoc! {"
29536 case \"$1\" in
29537 start)
29538 echo \"foo bar\"
29539 ;;
29540 stop)
29541 echo \"bar baz\"
29542 ;;
29543 esacˇ
29544 "});
29545
29546 // test `*)` auto outdents when typed inside `case` block
29547 cx.set_state(indoc! {"
29548 case \"$1\" in
29549 start)
29550 echo \"foo bar\"
29551 ;;
29552 ˇ
29553 "});
29554 cx.update_editor(|editor, window, cx| {
29555 editor.handle_input("*)", window, cx);
29556 });
29557 cx.wait_for_autoindent_applied().await;
29558 cx.assert_editor_state(indoc! {"
29559 case \"$1\" in
29560 start)
29561 echo \"foo bar\"
29562 ;;
29563 *)ˇ
29564 "});
29565
29566 // test `fi` outdents to correct level with nested if blocks
29567 cx.set_state(indoc! {"
29568 if [ \"$1\" = \"test\" ]; then
29569 echo \"outer if\"
29570 if [ \"$2\" = \"debug\" ]; then
29571 echo \"inner if\"
29572 ˇ
29573 "});
29574 cx.update_editor(|editor, window, cx| {
29575 editor.handle_input("fi", window, cx);
29576 });
29577 cx.wait_for_autoindent_applied().await;
29578 cx.assert_editor_state(indoc! {"
29579 if [ \"$1\" = \"test\" ]; then
29580 echo \"outer if\"
29581 if [ \"$2\" = \"debug\" ]; then
29582 echo \"inner if\"
29583 fiˇ
29584 "});
29585}
29586
29587#[gpui::test]
29588async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
29589 init_test(cx, |_| {});
29590 update_test_language_settings(cx, &|settings| {
29591 settings.defaults.extend_comment_on_newline = Some(false);
29592 });
29593 let mut cx = EditorTestContext::new(cx).await;
29594 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
29595 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
29596
29597 // test correct indent after newline on comment
29598 cx.set_state(indoc! {"
29599 # COMMENT:ˇ
29600 "});
29601 cx.update_editor(|editor, window, cx| {
29602 editor.newline(&Newline, window, cx);
29603 });
29604 cx.wait_for_autoindent_applied().await;
29605 cx.assert_editor_state(indoc! {"
29606 # COMMENT:
29607 ˇ
29608 "});
29609
29610 // test correct indent after newline after `then`
29611 cx.set_state(indoc! {"
29612
29613 if [ \"$1\" = \"test\" ]; thenˇ
29614 "});
29615 cx.update_editor(|editor, window, cx| {
29616 editor.newline(&Newline, window, cx);
29617 });
29618 cx.wait_for_autoindent_applied().await;
29619 cx.assert_editor_state(indoc! {"
29620
29621 if [ \"$1\" = \"test\" ]; then
29622 ˇ
29623 "});
29624
29625 // test correct indent after newline after `else`
29626 cx.set_state(indoc! {"
29627 if [ \"$1\" = \"test\" ]; then
29628 elseˇ
29629 "});
29630 cx.update_editor(|editor, window, cx| {
29631 editor.newline(&Newline, window, cx);
29632 });
29633 cx.wait_for_autoindent_applied().await;
29634 cx.assert_editor_state(indoc! {"
29635 if [ \"$1\" = \"test\" ]; then
29636 else
29637 ˇ
29638 "});
29639
29640 // test correct indent after newline after `elif`
29641 cx.set_state(indoc! {"
29642 if [ \"$1\" = \"test\" ]; then
29643 elifˇ
29644 "});
29645 cx.update_editor(|editor, window, cx| {
29646 editor.newline(&Newline, window, cx);
29647 });
29648 cx.wait_for_autoindent_applied().await;
29649 cx.assert_editor_state(indoc! {"
29650 if [ \"$1\" = \"test\" ]; then
29651 elif
29652 ˇ
29653 "});
29654
29655 // test correct indent after newline after `do`
29656 cx.set_state(indoc! {"
29657 for file in *.txt; doˇ
29658 "});
29659 cx.update_editor(|editor, window, cx| {
29660 editor.newline(&Newline, window, cx);
29661 });
29662 cx.wait_for_autoindent_applied().await;
29663 cx.assert_editor_state(indoc! {"
29664 for file in *.txt; do
29665 ˇ
29666 "});
29667
29668 // test correct indent after newline after case pattern
29669 cx.set_state(indoc! {"
29670 case \"$1\" in
29671 start)ˇ
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 case \"$1\" in
29679 start)
29680 ˇ
29681 "});
29682
29683 // test correct indent after newline after case pattern
29684 cx.set_state(indoc! {"
29685 case \"$1\" in
29686 start)
29687 ;;
29688 *)ˇ
29689 "});
29690 cx.update_editor(|editor, window, cx| {
29691 editor.newline(&Newline, window, cx);
29692 });
29693 cx.wait_for_autoindent_applied().await;
29694 cx.assert_editor_state(indoc! {"
29695 case \"$1\" in
29696 start)
29697 ;;
29698 *)
29699 ˇ
29700 "});
29701
29702 // test correct indent after newline after function opening brace
29703 cx.set_state(indoc! {"
29704 function test() {ˇ}
29705 "});
29706 cx.update_editor(|editor, window, cx| {
29707 editor.newline(&Newline, window, cx);
29708 });
29709 cx.wait_for_autoindent_applied().await;
29710 cx.assert_editor_state(indoc! {"
29711 function test() {
29712 ˇ
29713 }
29714 "});
29715
29716 // test no extra indent after semicolon on same line
29717 cx.set_state(indoc! {"
29718 echo \"test\";ˇ
29719 "});
29720 cx.update_editor(|editor, window, cx| {
29721 editor.newline(&Newline, window, cx);
29722 });
29723 cx.wait_for_autoindent_applied().await;
29724 cx.assert_editor_state(indoc! {"
29725 echo \"test\";
29726 ˇ
29727 "});
29728}
29729
29730fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
29731 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
29732 point..point
29733}
29734
29735#[track_caller]
29736fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
29737 let (text, ranges) = marked_text_ranges(marked_text, true);
29738 assert_eq!(editor.text(cx), text);
29739 assert_eq!(
29740 editor.selections.ranges(&editor.display_snapshot(cx)),
29741 ranges
29742 .iter()
29743 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
29744 .collect::<Vec<_>>(),
29745 "Assert selections are {}",
29746 marked_text
29747 );
29748}
29749
29750pub fn handle_signature_help_request(
29751 cx: &mut EditorLspTestContext,
29752 mocked_response: lsp::SignatureHelp,
29753) -> impl Future<Output = ()> + use<> {
29754 let mut request =
29755 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
29756 let mocked_response = mocked_response.clone();
29757 async move { Ok(Some(mocked_response)) }
29758 });
29759
29760 async move {
29761 request.next().await;
29762 }
29763}
29764
29765#[track_caller]
29766pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
29767 cx.update_editor(|editor, _, _| {
29768 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
29769 let entries = menu.entries.borrow();
29770 let entries = entries
29771 .iter()
29772 .map(|entry| entry.string.as_str())
29773 .collect::<Vec<_>>();
29774 assert_eq!(entries, expected);
29775 } else {
29776 panic!("Expected completions menu");
29777 }
29778 });
29779}
29780
29781#[gpui::test]
29782async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
29783 init_test(cx, |_| {});
29784 let mut cx = EditorLspTestContext::new_rust(
29785 lsp::ServerCapabilities {
29786 completion_provider: Some(lsp::CompletionOptions {
29787 ..Default::default()
29788 }),
29789 ..Default::default()
29790 },
29791 cx,
29792 )
29793 .await;
29794 cx.lsp
29795 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
29796 Ok(Some(lsp::CompletionResponse::Array(vec![
29797 lsp::CompletionItem {
29798 label: "unsafe".into(),
29799 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
29800 range: lsp::Range {
29801 start: lsp::Position {
29802 line: 0,
29803 character: 9,
29804 },
29805 end: lsp::Position {
29806 line: 0,
29807 character: 11,
29808 },
29809 },
29810 new_text: "unsafe".to_string(),
29811 })),
29812 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
29813 ..Default::default()
29814 },
29815 ])))
29816 });
29817
29818 cx.update_editor(|editor, _, cx| {
29819 editor.project().unwrap().update(cx, |project, cx| {
29820 project.snippets().update(cx, |snippets, _cx| {
29821 snippets.add_snippet_for_test(
29822 None,
29823 PathBuf::from("test_snippets.json"),
29824 vec![
29825 Arc::new(project::snippet_provider::Snippet {
29826 prefix: vec![
29827 "unlimited word count".to_string(),
29828 "unlimit word count".to_string(),
29829 "unlimited unknown".to_string(),
29830 ],
29831 body: "this is many words".to_string(),
29832 description: Some("description".to_string()),
29833 name: "multi-word snippet test".to_string(),
29834 }),
29835 Arc::new(project::snippet_provider::Snippet {
29836 prefix: vec!["unsnip".to_string(), "@few".to_string()],
29837 body: "fewer words".to_string(),
29838 description: Some("alt description".to_string()),
29839 name: "other name".to_string(),
29840 }),
29841 Arc::new(project::snippet_provider::Snippet {
29842 prefix: vec!["ab aa".to_string()],
29843 body: "abcd".to_string(),
29844 description: None,
29845 name: "alphabet".to_string(),
29846 }),
29847 ],
29848 );
29849 });
29850 })
29851 });
29852
29853 let get_completions = |cx: &mut EditorLspTestContext| {
29854 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
29855 Some(CodeContextMenu::Completions(context_menu)) => {
29856 let entries = context_menu.entries.borrow();
29857 entries
29858 .iter()
29859 .map(|entry| entry.string.clone())
29860 .collect_vec()
29861 }
29862 _ => vec![],
29863 })
29864 };
29865
29866 // snippets:
29867 // @foo
29868 // foo bar
29869 //
29870 // when typing:
29871 //
29872 // when typing:
29873 // - if I type a symbol "open the completions with snippets only"
29874 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
29875 //
29876 // stuff we need:
29877 // - filtering logic change?
29878 // - remember how far back the completion started.
29879
29880 let test_cases: &[(&str, &[&str])] = &[
29881 (
29882 "un",
29883 &[
29884 "unsafe",
29885 "unlimit word count",
29886 "unlimited unknown",
29887 "unlimited word count",
29888 "unsnip",
29889 ],
29890 ),
29891 (
29892 "u ",
29893 &[
29894 "unlimit word count",
29895 "unlimited unknown",
29896 "unlimited word count",
29897 ],
29898 ),
29899 ("u a", &["ab aa", "unsafe"]), // unsAfe
29900 (
29901 "u u",
29902 &[
29903 "unsafe",
29904 "unlimit word count",
29905 "unlimited unknown", // ranked highest among snippets
29906 "unlimited word count",
29907 "unsnip",
29908 ],
29909 ),
29910 ("uw c", &["unlimit word count", "unlimited word count"]),
29911 (
29912 "u w",
29913 &[
29914 "unlimit word count",
29915 "unlimited word count",
29916 "unlimited unknown",
29917 ],
29918 ),
29919 ("u w ", &["unlimit word count", "unlimited word count"]),
29920 (
29921 "u ",
29922 &[
29923 "unlimit word count",
29924 "unlimited unknown",
29925 "unlimited word count",
29926 ],
29927 ),
29928 ("wor", &[]),
29929 ("uf", &["unsafe"]),
29930 ("af", &["unsafe"]),
29931 ("afu", &[]),
29932 (
29933 "ue",
29934 &["unsafe", "unlimited unknown", "unlimited word count"],
29935 ),
29936 ("@", &["@few"]),
29937 ("@few", &["@few"]),
29938 ("@ ", &[]),
29939 ("a@", &["@few"]),
29940 ("a@f", &["@few", "unsafe"]),
29941 ("a@fw", &["@few"]),
29942 ("a", &["ab aa", "unsafe"]),
29943 ("aa", &["ab aa"]),
29944 ("aaa", &["ab aa"]),
29945 ("ab", &["ab aa"]),
29946 ("ab ", &["ab aa"]),
29947 ("ab a", &["ab aa", "unsafe"]),
29948 ("ab ab", &["ab aa"]),
29949 ("ab ab aa", &["ab aa"]),
29950 ];
29951
29952 for &(input_to_simulate, expected_completions) in test_cases {
29953 cx.set_state("fn a() { ˇ }\n");
29954 for c in input_to_simulate.split("") {
29955 cx.simulate_input(c);
29956 cx.run_until_parked();
29957 }
29958 let expected_completions = expected_completions
29959 .iter()
29960 .map(|s| s.to_string())
29961 .collect_vec();
29962 assert_eq!(
29963 get_completions(&mut cx),
29964 expected_completions,
29965 "< actual / expected >, input = {input_to_simulate:?}",
29966 );
29967 }
29968}
29969
29970/// Handle completion request passing a marked string specifying where the completion
29971/// should be triggered from using '|' character, what range should be replaced, and what completions
29972/// should be returned using '<' and '>' to delimit the range.
29973///
29974/// Also see `handle_completion_request_with_insert_and_replace`.
29975#[track_caller]
29976pub fn handle_completion_request(
29977 marked_string: &str,
29978 completions: Vec<&'static str>,
29979 is_incomplete: bool,
29980 counter: Arc<AtomicUsize>,
29981 cx: &mut EditorLspTestContext,
29982) -> impl Future<Output = ()> {
29983 let complete_from_marker: TextRangeMarker = '|'.into();
29984 let replace_range_marker: TextRangeMarker = ('<', '>').into();
29985 let (_, mut marked_ranges) = marked_text_ranges_by(
29986 marked_string,
29987 vec![complete_from_marker.clone(), replace_range_marker.clone()],
29988 );
29989
29990 let complete_from_position = cx.to_lsp(MultiBufferOffset(
29991 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
29992 ));
29993 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
29994 let replace_range =
29995 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
29996
29997 let mut request =
29998 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
29999 let completions = completions.clone();
30000 counter.fetch_add(1, atomic::Ordering::Release);
30001 async move {
30002 assert_eq!(params.text_document_position.text_document.uri, url.clone());
30003 assert_eq!(
30004 params.text_document_position.position,
30005 complete_from_position
30006 );
30007 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
30008 is_incomplete,
30009 item_defaults: None,
30010 items: completions
30011 .iter()
30012 .map(|completion_text| lsp::CompletionItem {
30013 label: completion_text.to_string(),
30014 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
30015 range: replace_range,
30016 new_text: completion_text.to_string(),
30017 })),
30018 ..Default::default()
30019 })
30020 .collect(),
30021 })))
30022 }
30023 });
30024
30025 async move {
30026 request.next().await;
30027 }
30028}
30029
30030/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
30031/// given instead, which also contains an `insert` range.
30032///
30033/// This function uses markers to define ranges:
30034/// - `|` marks the cursor position
30035/// - `<>` marks the replace range
30036/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
30037pub fn handle_completion_request_with_insert_and_replace(
30038 cx: &mut EditorLspTestContext,
30039 marked_string: &str,
30040 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
30041 counter: Arc<AtomicUsize>,
30042) -> impl Future<Output = ()> {
30043 let complete_from_marker: TextRangeMarker = '|'.into();
30044 let replace_range_marker: TextRangeMarker = ('<', '>').into();
30045 let insert_range_marker: TextRangeMarker = ('{', '}').into();
30046
30047 let (_, mut marked_ranges) = marked_text_ranges_by(
30048 marked_string,
30049 vec![
30050 complete_from_marker.clone(),
30051 replace_range_marker.clone(),
30052 insert_range_marker.clone(),
30053 ],
30054 );
30055
30056 let complete_from_position = cx.to_lsp(MultiBufferOffset(
30057 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
30058 ));
30059 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
30060 let replace_range =
30061 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
30062
30063 let insert_range = match marked_ranges.remove(&insert_range_marker) {
30064 Some(ranges) if !ranges.is_empty() => {
30065 let range1 = ranges[0].clone();
30066 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
30067 }
30068 _ => lsp::Range {
30069 start: replace_range.start,
30070 end: complete_from_position,
30071 },
30072 };
30073
30074 let mut request =
30075 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
30076 let completions = completions.clone();
30077 counter.fetch_add(1, atomic::Ordering::Release);
30078 async move {
30079 assert_eq!(params.text_document_position.text_document.uri, url.clone());
30080 assert_eq!(
30081 params.text_document_position.position, complete_from_position,
30082 "marker `|` position doesn't match",
30083 );
30084 Ok(Some(lsp::CompletionResponse::Array(
30085 completions
30086 .iter()
30087 .map(|(label, new_text)| lsp::CompletionItem {
30088 label: label.to_string(),
30089 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
30090 lsp::InsertReplaceEdit {
30091 insert: insert_range,
30092 replace: replace_range,
30093 new_text: new_text.to_string(),
30094 },
30095 )),
30096 ..Default::default()
30097 })
30098 .collect(),
30099 )))
30100 }
30101 });
30102
30103 async move {
30104 request.next().await;
30105 }
30106}
30107
30108fn handle_resolve_completion_request(
30109 cx: &mut EditorLspTestContext,
30110 edits: Option<Vec<(&'static str, &'static str)>>,
30111) -> impl Future<Output = ()> {
30112 let edits = edits.map(|edits| {
30113 edits
30114 .iter()
30115 .map(|(marked_string, new_text)| {
30116 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
30117 let replace_range = cx.to_lsp_range(
30118 MultiBufferOffset(marked_ranges[0].start)
30119 ..MultiBufferOffset(marked_ranges[0].end),
30120 );
30121 lsp::TextEdit::new(replace_range, new_text.to_string())
30122 })
30123 .collect::<Vec<_>>()
30124 });
30125
30126 let mut request =
30127 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
30128 let edits = edits.clone();
30129 async move {
30130 Ok(lsp::CompletionItem {
30131 additional_text_edits: edits,
30132 ..Default::default()
30133 })
30134 }
30135 });
30136
30137 async move {
30138 request.next().await;
30139 }
30140}
30141
30142pub(crate) fn update_test_language_settings(
30143 cx: &mut TestAppContext,
30144 f: &dyn Fn(&mut AllLanguageSettingsContent),
30145) {
30146 cx.update(|cx| {
30147 SettingsStore::update_global(cx, |store, cx| {
30148 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
30149 f(&mut settings.project.all_languages)
30150 });
30151 });
30152 });
30153}
30154
30155pub(crate) fn update_test_project_settings(
30156 cx: &mut TestAppContext,
30157 f: &dyn Fn(&mut ProjectSettingsContent),
30158) {
30159 cx.update(|cx| {
30160 SettingsStore::update_global(cx, |store, cx| {
30161 store.update_user_settings(cx, |settings| f(&mut settings.project));
30162 });
30163 });
30164}
30165
30166pub(crate) fn update_test_editor_settings(
30167 cx: &mut TestAppContext,
30168 f: &dyn Fn(&mut EditorSettingsContent),
30169) {
30170 cx.update(|cx| {
30171 SettingsStore::update_global(cx, |store, cx| {
30172 store.update_user_settings(cx, |settings| f(&mut settings.editor));
30173 })
30174 })
30175}
30176
30177pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
30178 cx.update(|cx| {
30179 assets::Assets.load_test_fonts(cx);
30180 let store = SettingsStore::test(cx);
30181 cx.set_global(store);
30182 theme_settings::init(theme::LoadThemes::JustBase, cx);
30183 release_channel::init(semver::Version::new(0, 0, 0), cx);
30184 crate::init(cx);
30185 });
30186 zlog::init_test();
30187 update_test_language_settings(cx, &f);
30188}
30189
30190#[track_caller]
30191fn assert_hunk_revert(
30192 not_reverted_text_with_selections: &str,
30193 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
30194 expected_reverted_text_with_selections: &str,
30195 base_text: &str,
30196 cx: &mut EditorLspTestContext,
30197) {
30198 cx.set_state(not_reverted_text_with_selections);
30199 cx.set_head_text(base_text);
30200 cx.executor().run_until_parked();
30201
30202 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
30203 let snapshot = editor.snapshot(window, cx);
30204 let reverted_hunk_statuses = snapshot
30205 .buffer_snapshot()
30206 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
30207 .map(|hunk| hunk.status().kind)
30208 .collect::<Vec<_>>();
30209
30210 editor.git_restore(&Default::default(), window, cx);
30211 reverted_hunk_statuses
30212 });
30213 cx.executor().run_until_parked();
30214 cx.assert_editor_state(expected_reverted_text_with_selections);
30215 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
30216}
30217
30218#[gpui::test(iterations = 10)]
30219async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
30220 init_test(cx, |_| {});
30221
30222 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
30223 let counter = diagnostic_requests.clone();
30224
30225 let fs = FakeFs::new(cx.executor());
30226 fs.insert_tree(
30227 path!("/a"),
30228 json!({
30229 "first.rs": "fn main() { let a = 5; }",
30230 "second.rs": "// Test file",
30231 }),
30232 )
30233 .await;
30234
30235 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
30236 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30237 let workspace = window
30238 .read_with(cx, |mw, _| mw.workspace().clone())
30239 .unwrap();
30240 let cx = &mut VisualTestContext::from_window(*window, cx);
30241
30242 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30243 language_registry.add(rust_lang());
30244 let mut fake_servers = language_registry.register_fake_lsp(
30245 "Rust",
30246 FakeLspAdapter {
30247 capabilities: lsp::ServerCapabilities {
30248 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
30249 lsp::DiagnosticOptions {
30250 identifier: None,
30251 inter_file_dependencies: true,
30252 workspace_diagnostics: true,
30253 work_done_progress_options: Default::default(),
30254 },
30255 )),
30256 ..Default::default()
30257 },
30258 ..Default::default()
30259 },
30260 );
30261
30262 let editor = workspace
30263 .update_in(cx, |workspace, window, cx| {
30264 workspace.open_abs_path(
30265 PathBuf::from(path!("/a/first.rs")),
30266 OpenOptions::default(),
30267 window,
30268 cx,
30269 )
30270 })
30271 .await
30272 .unwrap()
30273 .downcast::<Editor>()
30274 .unwrap();
30275 let fake_server = fake_servers.next().await.unwrap();
30276 let server_id = fake_server.server.server_id();
30277 let mut first_request = fake_server
30278 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
30279 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
30280 let result_id = Some(new_result_id.to_string());
30281 assert_eq!(
30282 params.text_document.uri,
30283 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
30284 );
30285 async move {
30286 Ok(lsp::DocumentDiagnosticReportResult::Report(
30287 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
30288 related_documents: None,
30289 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
30290 items: Vec::new(),
30291 result_id,
30292 },
30293 }),
30294 ))
30295 }
30296 });
30297
30298 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
30299 project.update(cx, |project, cx| {
30300 let buffer_id = editor
30301 .read(cx)
30302 .buffer()
30303 .read(cx)
30304 .as_singleton()
30305 .expect("created a singleton buffer")
30306 .read(cx)
30307 .remote_id();
30308 let buffer_result_id = project
30309 .lsp_store()
30310 .read(cx)
30311 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
30312 assert_eq!(expected_result_id, buffer_result_id);
30313 });
30314 };
30315
30316 ensure_result_id(None, cx);
30317 cx.executor().advance_clock(Duration::from_millis(60));
30318 cx.executor().run_until_parked();
30319 assert_eq!(
30320 diagnostic_requests.load(atomic::Ordering::Acquire),
30321 1,
30322 "Opening file should trigger diagnostic request"
30323 );
30324 first_request
30325 .next()
30326 .await
30327 .expect("should have sent the first diagnostics pull request");
30328 ensure_result_id(Some(SharedString::new_static("1")), cx);
30329
30330 // Editing should trigger diagnostics
30331 editor.update_in(cx, |editor, window, cx| {
30332 editor.handle_input("2", window, cx)
30333 });
30334 cx.executor().advance_clock(Duration::from_millis(60));
30335 cx.executor().run_until_parked();
30336 assert_eq!(
30337 diagnostic_requests.load(atomic::Ordering::Acquire),
30338 2,
30339 "Editing should trigger diagnostic request"
30340 );
30341 ensure_result_id(Some(SharedString::new_static("2")), cx);
30342
30343 // Moving cursor should not trigger diagnostic request
30344 editor.update_in(cx, |editor, window, cx| {
30345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30346 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
30347 });
30348 });
30349 cx.executor().advance_clock(Duration::from_millis(60));
30350 cx.executor().run_until_parked();
30351 assert_eq!(
30352 diagnostic_requests.load(atomic::Ordering::Acquire),
30353 2,
30354 "Cursor movement should not trigger diagnostic request"
30355 );
30356 ensure_result_id(Some(SharedString::new_static("2")), cx);
30357 // Multiple rapid edits should be debounced
30358 for _ in 0..5 {
30359 editor.update_in(cx, |editor, window, cx| {
30360 editor.handle_input("x", window, cx)
30361 });
30362 }
30363 cx.executor().advance_clock(Duration::from_millis(60));
30364 cx.executor().run_until_parked();
30365
30366 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
30367 assert!(
30368 final_requests <= 4,
30369 "Multiple rapid edits should be debounced (got {final_requests} requests)",
30370 );
30371 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
30372}
30373
30374#[gpui::test]
30375async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
30376 // Regression test for issue #11671
30377 // Previously, adding a cursor after moving multiple cursors would reset
30378 // the cursor count instead of adding to the existing cursors.
30379 init_test(cx, |_| {});
30380 let mut cx = EditorTestContext::new(cx).await;
30381
30382 // Create a simple buffer with cursor at start
30383 cx.set_state(indoc! {"
30384 ˇaaaa
30385 bbbb
30386 cccc
30387 dddd
30388 eeee
30389 ffff
30390 gggg
30391 hhhh"});
30392
30393 // Add 2 cursors below (so we have 3 total)
30394 cx.update_editor(|editor, window, cx| {
30395 editor.add_selection_below(&Default::default(), window, cx);
30396 editor.add_selection_below(&Default::default(), window, cx);
30397 });
30398
30399 // Verify we have 3 cursors
30400 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
30401 assert_eq!(
30402 initial_count, 3,
30403 "Should have 3 cursors after adding 2 below"
30404 );
30405
30406 // Move down one line
30407 cx.update_editor(|editor, window, cx| {
30408 editor.move_down(&MoveDown, window, cx);
30409 });
30410
30411 // Add another cursor below
30412 cx.update_editor(|editor, window, cx| {
30413 editor.add_selection_below(&Default::default(), window, cx);
30414 });
30415
30416 // Should now have 4 cursors (3 original + 1 new)
30417 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
30418 assert_eq!(
30419 final_count, 4,
30420 "Should have 4 cursors after moving and adding another"
30421 );
30422}
30423
30424#[gpui::test]
30425async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
30426 init_test(cx, |_| {});
30427
30428 let mut cx = EditorTestContext::new(cx).await;
30429
30430 cx.set_state(indoc!(
30431 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
30432 Second line here"#
30433 ));
30434
30435 cx.update_editor(|editor, window, cx| {
30436 // Enable soft wrapping with a narrow width to force soft wrapping and
30437 // confirm that more than 2 rows are being displayed.
30438 editor.set_wrap_width(Some(100.0.into()), cx);
30439 assert!(editor.display_text(cx).lines().count() > 2);
30440
30441 editor.add_selection_below(
30442 &AddSelectionBelow {
30443 skip_soft_wrap: true,
30444 },
30445 window,
30446 cx,
30447 );
30448
30449 assert_eq!(
30450 display_ranges(editor, cx),
30451 &[
30452 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
30453 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
30454 ]
30455 );
30456
30457 editor.add_selection_above(
30458 &AddSelectionAbove {
30459 skip_soft_wrap: true,
30460 },
30461 window,
30462 cx,
30463 );
30464
30465 assert_eq!(
30466 display_ranges(editor, cx),
30467 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
30468 );
30469
30470 editor.add_selection_below(
30471 &AddSelectionBelow {
30472 skip_soft_wrap: false,
30473 },
30474 window,
30475 cx,
30476 );
30477
30478 assert_eq!(
30479 display_ranges(editor, cx),
30480 &[
30481 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
30482 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
30483 ]
30484 );
30485
30486 editor.add_selection_above(
30487 &AddSelectionAbove {
30488 skip_soft_wrap: false,
30489 },
30490 window,
30491 cx,
30492 );
30493
30494 assert_eq!(
30495 display_ranges(editor, cx),
30496 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
30497 );
30498 });
30499
30500 // Set up text where selections are in the middle of a soft-wrapped line.
30501 // When adding selection below with `skip_soft_wrap` set to `true`, the new
30502 // selection should be at the same buffer column, not the same pixel
30503 // position.
30504 cx.set_state(indoc!(
30505 r#"1. Very long line to show «howˇ» a wrapped line would look
30506 2. Very long line to show how a wrapped line would look"#
30507 ));
30508
30509 cx.update_editor(|editor, window, cx| {
30510 // Enable soft wrapping with a narrow width to force soft wrapping and
30511 // confirm that more than 2 rows are being displayed.
30512 editor.set_wrap_width(Some(100.0.into()), cx);
30513 assert!(editor.display_text(cx).lines().count() > 2);
30514
30515 editor.add_selection_below(
30516 &AddSelectionBelow {
30517 skip_soft_wrap: true,
30518 },
30519 window,
30520 cx,
30521 );
30522
30523 // Assert that there's now 2 selections, both selecting the same column
30524 // range in the buffer row.
30525 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
30526 let selections = editor.selections.all::<Point>(&display_map);
30527 assert_eq!(selections.len(), 2);
30528 assert_eq!(selections[0].start.column, selections[1].start.column);
30529 assert_eq!(selections[0].end.column, selections[1].end.column);
30530 });
30531}
30532
30533#[gpui::test]
30534async fn test_insert_snippet(cx: &mut TestAppContext) {
30535 init_test(cx, |_| {});
30536 let mut cx = EditorTestContext::new(cx).await;
30537
30538 cx.update_editor(|editor, _, cx| {
30539 editor.project().unwrap().update(cx, |project, cx| {
30540 project.snippets().update(cx, |snippets, _cx| {
30541 let snippet = project::snippet_provider::Snippet {
30542 prefix: vec![], // no prefix needed!
30543 body: "an Unspecified".to_string(),
30544 description: Some("shhhh it's a secret".to_string()),
30545 name: "super secret snippet".to_string(),
30546 };
30547 snippets.add_snippet_for_test(
30548 None,
30549 PathBuf::from("test_snippets.json"),
30550 vec![Arc::new(snippet)],
30551 );
30552
30553 let snippet = project::snippet_provider::Snippet {
30554 prefix: vec![], // no prefix needed!
30555 body: " Location".to_string(),
30556 description: Some("the word 'location'".to_string()),
30557 name: "location word".to_string(),
30558 };
30559 snippets.add_snippet_for_test(
30560 Some("Markdown".to_string()),
30561 PathBuf::from("test_snippets.json"),
30562 vec![Arc::new(snippet)],
30563 );
30564 });
30565 })
30566 });
30567
30568 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
30569
30570 cx.update_editor(|editor, window, cx| {
30571 editor.insert_snippet_at_selections(
30572 &InsertSnippet {
30573 language: None,
30574 name: Some("super secret snippet".to_string()),
30575 snippet: None,
30576 },
30577 window,
30578 cx,
30579 );
30580
30581 // Language is specified in the action,
30582 // so the buffer language does not need to match
30583 editor.insert_snippet_at_selections(
30584 &InsertSnippet {
30585 language: Some("Markdown".to_string()),
30586 name: Some("location word".to_string()),
30587 snippet: None,
30588 },
30589 window,
30590 cx,
30591 );
30592
30593 editor.insert_snippet_at_selections(
30594 &InsertSnippet {
30595 language: None,
30596 name: None,
30597 snippet: Some("$0 after".to_string()),
30598 },
30599 window,
30600 cx,
30601 );
30602 });
30603
30604 cx.assert_editor_state(
30605 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
30606 );
30607}
30608
30609#[gpui::test]
30610async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
30611 use crate::inlays::inlay_hints::InlayHintRefreshReason;
30612 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
30613 use settings::InlayHintSettingsContent;
30614 use std::sync::atomic::AtomicU32;
30615 use std::time::Duration;
30616
30617 const BASE_TIMEOUT_SECS: u64 = 1;
30618
30619 let request_count = Arc::new(AtomicU32::new(0));
30620 let closure_request_count = request_count.clone();
30621
30622 init_test(cx, &|settings| {
30623 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
30624 enabled: Some(true),
30625 ..InlayHintSettingsContent::default()
30626 })
30627 });
30628 cx.update(|cx| {
30629 SettingsStore::update_global(cx, |store, cx| {
30630 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
30631 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
30632 request_timeout: Some(BASE_TIMEOUT_SECS),
30633 button: Some(true),
30634 notifications: None,
30635 semantic_token_rules: None,
30636 });
30637 });
30638 });
30639 });
30640
30641 let fs = FakeFs::new(cx.executor());
30642 fs.insert_tree(
30643 path!("/a"),
30644 json!({
30645 "main.rs": "fn main() { let a = 5; }",
30646 }),
30647 )
30648 .await;
30649
30650 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
30651 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30652 language_registry.add(rust_lang());
30653 let mut fake_servers = language_registry.register_fake_lsp(
30654 "Rust",
30655 FakeLspAdapter {
30656 capabilities: lsp::ServerCapabilities {
30657 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30658 ..lsp::ServerCapabilities::default()
30659 },
30660 initializer: Some(Box::new(move |fake_server| {
30661 let request_count = closure_request_count.clone();
30662 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30663 move |params, cx| {
30664 let request_count = request_count.clone();
30665 async move {
30666 cx.background_executor()
30667 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
30668 .await;
30669 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
30670 assert_eq!(
30671 params.text_document.uri,
30672 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
30673 );
30674 Ok(Some(vec![lsp::InlayHint {
30675 position: lsp::Position::new(0, 1),
30676 label: lsp::InlayHintLabel::String(count.to_string()),
30677 kind: None,
30678 text_edits: None,
30679 tooltip: None,
30680 padding_left: None,
30681 padding_right: None,
30682 data: None,
30683 }]))
30684 }
30685 },
30686 );
30687 })),
30688 ..FakeLspAdapter::default()
30689 },
30690 );
30691
30692 let buffer = project
30693 .update(cx, |project, cx| {
30694 project.open_local_buffer(path!("/a/main.rs"), cx)
30695 })
30696 .await
30697 .unwrap();
30698 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
30699
30700 cx.executor().run_until_parked();
30701 let fake_server = fake_servers.next().await.unwrap();
30702
30703 cx.executor()
30704 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
30705 cx.executor().run_until_parked();
30706 editor
30707 .update(cx, |editor, _window, cx| {
30708 assert!(
30709 cached_hint_labels(editor, cx).is_empty(),
30710 "First request should time out, no hints cached"
30711 );
30712 })
30713 .unwrap();
30714
30715 editor
30716 .update(cx, |editor, _window, cx| {
30717 editor.refresh_inlay_hints(
30718 InlayHintRefreshReason::RefreshRequested {
30719 server_id: fake_server.server.server_id(),
30720 request_id: Some(1),
30721 },
30722 cx,
30723 );
30724 })
30725 .unwrap();
30726 cx.executor()
30727 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
30728 cx.executor().run_until_parked();
30729 editor
30730 .update(cx, |editor, _window, cx| {
30731 assert!(
30732 cached_hint_labels(editor, cx).is_empty(),
30733 "Second request should also time out with BASE_TIMEOUT, no hints cached"
30734 );
30735 })
30736 .unwrap();
30737
30738 cx.update(|cx| {
30739 SettingsStore::update_global(cx, |store, cx| {
30740 store.update_user_settings(cx, |settings| {
30741 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
30742 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
30743 button: Some(true),
30744 notifications: None,
30745 semantic_token_rules: None,
30746 });
30747 });
30748 });
30749 });
30750 editor
30751 .update(cx, |editor, _window, cx| {
30752 editor.refresh_inlay_hints(
30753 InlayHintRefreshReason::RefreshRequested {
30754 server_id: fake_server.server.server_id(),
30755 request_id: Some(2),
30756 },
30757 cx,
30758 );
30759 })
30760 .unwrap();
30761 cx.executor()
30762 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
30763 cx.executor().run_until_parked();
30764 editor
30765 .update(cx, |editor, _window, cx| {
30766 assert_eq!(
30767 vec!["1".to_string()],
30768 cached_hint_labels(editor, cx),
30769 "With extended timeout (BASE * 4), hints should arrive successfully"
30770 );
30771 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
30772 })
30773 .unwrap();
30774}
30775
30776#[gpui::test]
30777async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
30778 init_test(cx, |_| {});
30779 let (editor, cx) = cx.add_window_view(Editor::single_line);
30780 editor.update_in(cx, |editor, window, cx| {
30781 editor.set_text("oops\n\nwow\n", window, cx)
30782 });
30783 cx.run_until_parked();
30784 editor.update(cx, |editor, cx| {
30785 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
30786 });
30787 editor.update(cx, |editor, cx| {
30788 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
30789 });
30790 cx.run_until_parked();
30791 editor.update(cx, |editor, cx| {
30792 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
30793 });
30794}
30795
30796#[gpui::test]
30797async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
30798 init_test(cx, |_| {});
30799
30800 cx.update(|cx| {
30801 register_project_item::<Editor>(cx);
30802 });
30803
30804 let fs = FakeFs::new(cx.executor());
30805 fs.insert_tree("/root1", json!({})).await;
30806 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
30807 .await;
30808
30809 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
30810 let (multi_workspace, cx) =
30811 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30812 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
30813
30814 let worktree_id = project.update(cx, |project, cx| {
30815 project.worktrees(cx).next().unwrap().read(cx).id()
30816 });
30817
30818 let handle = workspace
30819 .update_in(cx, |workspace, window, cx| {
30820 let project_path = (worktree_id, rel_path("one.pdf"));
30821 workspace.open_path(project_path, None, true, window, cx)
30822 })
30823 .await
30824 .unwrap();
30825 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
30826 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
30827 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
30828 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
30829}
30830
30831#[gpui::test]
30832async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
30833 init_test(cx, |_| {});
30834
30835 let language = Arc::new(Language::new(
30836 LanguageConfig::default(),
30837 Some(tree_sitter_rust::LANGUAGE.into()),
30838 ));
30839
30840 // Test hierarchical sibling navigation
30841 let text = r#"
30842 fn outer() {
30843 if condition {
30844 let a = 1;
30845 }
30846 let b = 2;
30847 }
30848
30849 fn another() {
30850 let c = 3;
30851 }
30852 "#;
30853
30854 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
30855 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30856 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
30857
30858 // Wait for parsing to complete
30859 editor
30860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
30861 .await;
30862
30863 editor.update_in(cx, |editor, window, cx| {
30864 // Start by selecting "let a = 1;" inside the if block
30865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30866 s.select_display_ranges([
30867 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
30868 ]);
30869 });
30870
30871 let initial_selection = editor
30872 .selections
30873 .display_ranges(&editor.display_snapshot(cx));
30874 assert_eq!(initial_selection.len(), 1, "Should have one selection");
30875
30876 // Test select next sibling - should move up levels to find the next sibling
30877 // Since "let a = 1;" has no siblings in the if block, it should move up
30878 // to find "let b = 2;" which is a sibling of the if block
30879 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
30880 let next_selection = editor
30881 .selections
30882 .display_ranges(&editor.display_snapshot(cx));
30883
30884 // Should have a selection and it should be different from the initial
30885 assert_eq!(
30886 next_selection.len(),
30887 1,
30888 "Should have one selection after next"
30889 );
30890 assert_ne!(
30891 next_selection[0], initial_selection[0],
30892 "Next sibling selection should be different"
30893 );
30894
30895 // Test hierarchical navigation by going to the end of the current function
30896 // and trying to navigate to the next function
30897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30898 s.select_display_ranges([
30899 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
30900 ]);
30901 });
30902
30903 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
30904 let function_next_selection = editor
30905 .selections
30906 .display_ranges(&editor.display_snapshot(cx));
30907
30908 // Should move to the next function
30909 assert_eq!(
30910 function_next_selection.len(),
30911 1,
30912 "Should have one selection after function next"
30913 );
30914
30915 // Test select previous sibling navigation
30916 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
30917 let prev_selection = editor
30918 .selections
30919 .display_ranges(&editor.display_snapshot(cx));
30920
30921 // Should have a selection and it should be different
30922 assert_eq!(
30923 prev_selection.len(),
30924 1,
30925 "Should have one selection after prev"
30926 );
30927 assert_ne!(
30928 prev_selection[0], function_next_selection[0],
30929 "Previous sibling selection should be different from next"
30930 );
30931 });
30932}
30933
30934#[gpui::test]
30935async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
30936 init_test(cx, |_| {});
30937
30938 let mut cx = EditorTestContext::new(cx).await;
30939 cx.set_state(
30940 "let ˇvariable = 42;
30941let another = variable + 1;
30942let result = variable * 2;",
30943 );
30944
30945 // Set up document highlights manually (simulating LSP response)
30946 cx.update_editor(|editor, _window, cx| {
30947 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
30948
30949 // Create highlights for "variable" occurrences
30950 let highlight_ranges = [
30951 Point::new(0, 4)..Point::new(0, 12), // First "variable"
30952 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
30953 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
30954 ];
30955
30956 let anchor_ranges: Vec<_> = highlight_ranges
30957 .iter()
30958 .map(|range| range.clone().to_anchors(&buffer_snapshot))
30959 .collect();
30960
30961 editor.highlight_background(
30962 HighlightKey::DocumentHighlightRead,
30963 &anchor_ranges,
30964 |_, theme| theme.colors().editor_document_highlight_read_background,
30965 cx,
30966 );
30967 });
30968
30969 // Go to next highlight - should move to second "variable"
30970 cx.update_editor(|editor, window, cx| {
30971 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
30972 });
30973 cx.assert_editor_state(
30974 "let variable = 42;
30975let another = ˇvariable + 1;
30976let result = variable * 2;",
30977 );
30978
30979 // Go to next highlight - should move to third "variable"
30980 cx.update_editor(|editor, window, cx| {
30981 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
30982 });
30983 cx.assert_editor_state(
30984 "let variable = 42;
30985let another = variable + 1;
30986let result = ˇvariable * 2;",
30987 );
30988
30989 // Go to next highlight - should stay at third "variable" (no wrap-around)
30990 cx.update_editor(|editor, window, cx| {
30991 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
30992 });
30993 cx.assert_editor_state(
30994 "let variable = 42;
30995let another = variable + 1;
30996let result = ˇvariable * 2;",
30997 );
30998
30999 // Now test going backwards from third position
31000 cx.update_editor(|editor, window, cx| {
31001 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31002 });
31003 cx.assert_editor_state(
31004 "let variable = 42;
31005let another = ˇvariable + 1;
31006let result = variable * 2;",
31007 );
31008
31009 // Go to previous highlight - should move to first "variable"
31010 cx.update_editor(|editor, window, cx| {
31011 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31012 });
31013 cx.assert_editor_state(
31014 "let ˇvariable = 42;
31015let another = variable + 1;
31016let result = variable * 2;",
31017 );
31018
31019 // Go to previous highlight - should stay on first "variable"
31020 cx.update_editor(|editor, window, cx| {
31021 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
31022 });
31023 cx.assert_editor_state(
31024 "let ˇvariable = 42;
31025let another = variable + 1;
31026let result = variable * 2;",
31027 );
31028}
31029
31030#[gpui::test]
31031async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
31032 cx: &mut gpui::TestAppContext,
31033) {
31034 init_test(cx, |_| {});
31035
31036 let url = "https://zed.dev";
31037
31038 let markdown_language = Arc::new(Language::new(
31039 LanguageConfig {
31040 name: "Markdown".into(),
31041 ..LanguageConfig::default()
31042 },
31043 None,
31044 ));
31045
31046 let mut cx = EditorTestContext::new(cx).await;
31047 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31048 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
31049
31050 cx.update_editor(|editor, window, cx| {
31051 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31052 editor.paste(&Paste, window, cx);
31053 });
31054
31055 cx.assert_editor_state(&format!(
31056 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
31057 ));
31058}
31059
31060#[gpui::test]
31061async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
31062 init_test(cx, |_| {});
31063
31064 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31065 let mut cx = EditorTestContext::new(cx).await;
31066
31067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31068
31069 // Case 1: Test if adding a character with multi cursors preserves nested list indents
31070 cx.set_state(&indoc! {"
31071 - [ ] Item 1
31072 - [ ] Item 1.a
31073 - [ˇ] Item 2
31074 - [ˇ] Item 2.a
31075 - [ˇ] Item 2.b
31076 "
31077 });
31078 cx.update_editor(|editor, window, cx| {
31079 editor.handle_input("x", window, cx);
31080 });
31081 cx.run_until_parked();
31082 cx.assert_editor_state(indoc! {"
31083 - [ ] Item 1
31084 - [ ] Item 1.a
31085 - [xˇ] Item 2
31086 - [xˇ] Item 2.a
31087 - [xˇ] Item 2.b
31088 "
31089 });
31090
31091 // Case 2: Test adding new line after nested list continues the list with unchecked task
31092 cx.set_state(&indoc! {"
31093 - [ ] Item 1
31094 - [ ] Item 1.a
31095 - [x] Item 2
31096 - [x] Item 2.a
31097 - [x] Item 2.bˇ"
31098 });
31099 cx.update_editor(|editor, window, cx| {
31100 editor.newline(&Newline, window, cx);
31101 });
31102 cx.assert_editor_state(indoc! {"
31103 - [ ] Item 1
31104 - [ ] Item 1.a
31105 - [x] Item 2
31106 - [x] Item 2.a
31107 - [x] Item 2.b
31108 - [ ] ˇ"
31109 });
31110
31111 // Case 3: Test adding content to continued list item
31112 cx.update_editor(|editor, window, cx| {
31113 editor.handle_input("Item 2.c", window, cx);
31114 });
31115 cx.run_until_parked();
31116 cx.assert_editor_state(indoc! {"
31117 - [ ] Item 1
31118 - [ ] Item 1.a
31119 - [x] Item 2
31120 - [x] Item 2.a
31121 - [x] Item 2.b
31122 - [ ] Item 2.cˇ"
31123 });
31124
31125 // Case 4: Test adding new line after nested ordered list continues with next number
31126 cx.set_state(indoc! {"
31127 1. Item 1
31128 1. Item 1.a
31129 2. Item 2
31130 1. Item 2.a
31131 2. Item 2.bˇ"
31132 });
31133 cx.update_editor(|editor, window, cx| {
31134 editor.newline(&Newline, window, cx);
31135 });
31136 cx.assert_editor_state(indoc! {"
31137 1. Item 1
31138 1. Item 1.a
31139 2. Item 2
31140 1. Item 2.a
31141 2. Item 2.b
31142 3. ˇ"
31143 });
31144
31145 // Case 5: Adding content to continued ordered list item
31146 cx.update_editor(|editor, window, cx| {
31147 editor.handle_input("Item 2.c", window, cx);
31148 });
31149 cx.run_until_parked();
31150 cx.assert_editor_state(indoc! {"
31151 1. Item 1
31152 1. Item 1.a
31153 2. Item 2
31154 1. Item 2.a
31155 2. Item 2.b
31156 3. Item 2.cˇ"
31157 });
31158
31159 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
31160 cx.set_state(indoc! {"
31161 - Item 1
31162 - Item 1.a
31163 - Item 1.a
31164 ˇ"});
31165 cx.update_editor(|editor, window, cx| {
31166 editor.handle_input("-", window, cx);
31167 });
31168 cx.run_until_parked();
31169 cx.assert_editor_state(indoc! {"
31170 - Item 1
31171 - Item 1.a
31172 - Item 1.a
31173 -ˇ"});
31174
31175 // Case 7: Test blockquote newline preserves something
31176 cx.set_state(indoc! {"
31177 > Item 1ˇ"
31178 });
31179 cx.update_editor(|editor, window, cx| {
31180 editor.newline(&Newline, window, cx);
31181 });
31182 cx.assert_editor_state(indoc! {"
31183 > Item 1
31184 ˇ"
31185 });
31186}
31187
31188#[gpui::test]
31189async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
31190 cx: &mut gpui::TestAppContext,
31191) {
31192 init_test(cx, |_| {});
31193
31194 let url = "https://zed.dev";
31195
31196 let markdown_language = Arc::new(Language::new(
31197 LanguageConfig {
31198 name: "Markdown".into(),
31199 ..LanguageConfig::default()
31200 },
31201 None,
31202 ));
31203
31204 let mut cx = EditorTestContext::new(cx).await;
31205 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31206 cx.set_state(&format!(
31207 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
31208 ));
31209
31210 cx.update_editor(|editor, window, cx| {
31211 editor.copy(&Copy, window, cx);
31212 });
31213
31214 cx.set_state(&format!(
31215 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
31216 ));
31217
31218 cx.update_editor(|editor, window, cx| {
31219 editor.paste(&Paste, window, cx);
31220 });
31221
31222 cx.assert_editor_state(&format!(
31223 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
31224 ));
31225}
31226
31227#[gpui::test]
31228async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
31229 cx: &mut gpui::TestAppContext,
31230) {
31231 init_test(cx, |_| {});
31232
31233 let url = "https://zed.dev";
31234
31235 let markdown_language = Arc::new(Language::new(
31236 LanguageConfig {
31237 name: "Markdown".into(),
31238 ..LanguageConfig::default()
31239 },
31240 None,
31241 ));
31242
31243 let mut cx = EditorTestContext::new(cx).await;
31244 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31245 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
31246
31247 cx.update_editor(|editor, window, cx| {
31248 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31249 editor.paste(&Paste, window, cx);
31250 });
31251
31252 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
31253}
31254
31255#[gpui::test]
31256async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
31257 cx: &mut gpui::TestAppContext,
31258) {
31259 init_test(cx, |_| {});
31260
31261 let text = "Awesome";
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("Hello, «editorˇ».\nZed is «ˇgreat»");
31274
31275 cx.update_editor(|editor, window, cx| {
31276 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
31277 editor.paste(&Paste, window, cx);
31278 });
31279
31280 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
31281}
31282
31283#[gpui::test]
31284async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
31285 cx: &mut gpui::TestAppContext,
31286) {
31287 init_test(cx, |_| {});
31288
31289 let url = "https://zed.dev";
31290
31291 let markdown_language = Arc::new(Language::new(
31292 LanguageConfig {
31293 name: "Rust".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ˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
31302
31303 cx.update_editor(|editor, window, cx| {
31304 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31305 editor.paste(&Paste, window, cx);
31306 });
31307
31308 cx.assert_editor_state(&format!(
31309 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
31310 ));
31311}
31312
31313#[gpui::test]
31314async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
31315 cx: &mut TestAppContext,
31316) {
31317 init_test(cx, |_| {});
31318
31319 let url = "https://zed.dev";
31320
31321 let markdown_language = Arc::new(Language::new(
31322 LanguageConfig {
31323 name: "Markdown".into(),
31324 ..LanguageConfig::default()
31325 },
31326 None,
31327 ));
31328
31329 let (editor, cx) = cx.add_window_view(|window, cx| {
31330 let multi_buffer = MultiBuffer::build_multi(
31331 [
31332 ("this will embed -> link", vec![Point::row_range(0..1)]),
31333 ("this will replace -> link", vec![Point::row_range(0..1)]),
31334 ],
31335 cx,
31336 );
31337 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
31338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31339 s.select_ranges(vec![
31340 Point::new(0, 19)..Point::new(0, 23),
31341 Point::new(1, 21)..Point::new(1, 25),
31342 ])
31343 });
31344 let snapshot = multi_buffer.read(cx).snapshot(cx);
31345 let first_buffer_id = snapshot.all_buffer_ids().next().unwrap();
31346 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
31347 first_buffer.update(cx, |buffer, cx| {
31348 buffer.set_language(Some(markdown_language.clone()), cx);
31349 });
31350
31351 editor
31352 });
31353 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
31354
31355 cx.update_editor(|editor, window, cx| {
31356 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
31357 editor.paste(&Paste, window, cx);
31358 });
31359
31360 cx.assert_editor_state(&format!(
31361 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
31362 ));
31363}
31364
31365#[gpui::test]
31366async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
31367 init_test(cx, |_| {});
31368
31369 let fs = FakeFs::new(cx.executor());
31370 fs.insert_tree(
31371 path!("/project"),
31372 json!({
31373 "first.rs": "# First Document\nSome content here.",
31374 "second.rs": "Plain text content for second file.",
31375 }),
31376 )
31377 .await;
31378
31379 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
31380 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31381 let cx = &mut VisualTestContext::from_window(*window, cx);
31382
31383 let language = rust_lang();
31384 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
31385 language_registry.add(language.clone());
31386 let mut fake_servers = language_registry.register_fake_lsp(
31387 "Rust",
31388 FakeLspAdapter {
31389 ..FakeLspAdapter::default()
31390 },
31391 );
31392
31393 let buffer1 = project
31394 .update(cx, |project, cx| {
31395 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
31396 })
31397 .await
31398 .unwrap();
31399 let buffer2 = project
31400 .update(cx, |project, cx| {
31401 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
31402 })
31403 .await
31404 .unwrap();
31405
31406 let multi_buffer = cx.new(|cx| {
31407 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
31408 multi_buffer.set_excerpts_for_path(
31409 PathKey::for_buffer(&buffer1, cx),
31410 buffer1.clone(),
31411 [Point::zero()..buffer1.read(cx).max_point()],
31412 3,
31413 cx,
31414 );
31415 multi_buffer.set_excerpts_for_path(
31416 PathKey::for_buffer(&buffer2, cx),
31417 buffer2.clone(),
31418 [Point::zero()..buffer1.read(cx).max_point()],
31419 3,
31420 cx,
31421 );
31422 multi_buffer
31423 });
31424
31425 let (editor, cx) = cx.add_window_view(|window, cx| {
31426 Editor::new(
31427 EditorMode::full(),
31428 multi_buffer,
31429 Some(project.clone()),
31430 window,
31431 cx,
31432 )
31433 });
31434
31435 let fake_language_server = fake_servers.next().await.unwrap();
31436
31437 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
31438
31439 let save = editor.update_in(cx, |editor, window, cx| {
31440 assert!(editor.is_dirty(cx));
31441
31442 editor.save(
31443 SaveOptions {
31444 format: true,
31445 autosave: true,
31446 },
31447 project,
31448 window,
31449 cx,
31450 )
31451 });
31452 let (start_edit_tx, start_edit_rx) = oneshot::channel();
31453 let (done_edit_tx, done_edit_rx) = oneshot::channel();
31454 let mut done_edit_rx = Some(done_edit_rx);
31455 let mut start_edit_tx = Some(start_edit_tx);
31456
31457 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
31458 start_edit_tx.take().unwrap().send(()).unwrap();
31459 let done_edit_rx = done_edit_rx.take().unwrap();
31460 async move {
31461 done_edit_rx.await.unwrap();
31462 Ok(None)
31463 }
31464 });
31465
31466 start_edit_rx.await.unwrap();
31467 buffer2
31468 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
31469 .unwrap();
31470
31471 done_edit_tx.send(()).unwrap();
31472
31473 save.await.unwrap();
31474 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
31475}
31476
31477#[gpui::test]
31478fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
31479 init_test(cx, |_| {});
31480
31481 let editor = cx.add_window(|window, cx| {
31482 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
31483 build_editor(buffer, window, cx)
31484 });
31485
31486 editor
31487 .update(cx, |editor, window, cx| {
31488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31489 s.select_display_ranges([
31490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
31491 ])
31492 });
31493
31494 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
31495
31496 assert_eq!(
31497 editor.display_text(cx),
31498 "line1\nline2\nline2",
31499 "Duplicating last line upward should create duplicate above, not on same line"
31500 );
31501
31502 assert_eq!(
31503 editor
31504 .selections
31505 .display_ranges(&editor.display_snapshot(cx)),
31506 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
31507 "Selection should move to the duplicated line"
31508 );
31509 })
31510 .unwrap();
31511}
31512
31513#[gpui::test]
31514async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
31515 init_test(cx, |_| {});
31516
31517 let mut cx = EditorTestContext::new(cx).await;
31518
31519 cx.set_state("line1\nline2ˇ");
31520
31521 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
31522
31523 let clipboard_text = cx
31524 .read_from_clipboard()
31525 .and_then(|item| item.text().as_deref().map(str::to_string));
31526
31527 assert_eq!(
31528 clipboard_text,
31529 Some("line2\n".to_string()),
31530 "Copying a line without trailing newline should include a newline"
31531 );
31532
31533 cx.set_state("line1\nˇ");
31534
31535 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31536
31537 cx.assert_editor_state("line1\nline2\nˇ");
31538}
31539
31540#[gpui::test]
31541async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
31542 init_test(cx, |_| {});
31543
31544 let mut cx = EditorTestContext::new(cx).await;
31545
31546 cx.set_state("ˇline1\nˇline2\nˇline3\n");
31547
31548 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
31549
31550 let clipboard_text = cx
31551 .read_from_clipboard()
31552 .and_then(|item| item.text().as_deref().map(str::to_string));
31553
31554 assert_eq!(
31555 clipboard_text,
31556 Some("line1\nline2\nline3\n".to_string()),
31557 "Copying multiple lines should include a single newline between lines"
31558 );
31559
31560 cx.set_state("lineA\nˇ");
31561
31562 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31563
31564 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
31565}
31566
31567#[gpui::test]
31568async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
31569 init_test(cx, |_| {});
31570
31571 let mut cx = EditorTestContext::new(cx).await;
31572
31573 cx.set_state("ˇline1\nˇline2\nˇline3\n");
31574
31575 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
31576
31577 let clipboard_text = cx
31578 .read_from_clipboard()
31579 .and_then(|item| item.text().as_deref().map(str::to_string));
31580
31581 assert_eq!(
31582 clipboard_text,
31583 Some("line1\nline2\nline3\n".to_string()),
31584 "Copying multiple lines should include a single newline between lines"
31585 );
31586
31587 cx.set_state("lineA\nˇ");
31588
31589 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
31590
31591 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
31592}
31593
31594#[gpui::test]
31595async fn test_end_of_editor_context(cx: &mut TestAppContext) {
31596 init_test(cx, |_| {});
31597
31598 let mut cx = EditorTestContext::new(cx).await;
31599
31600 cx.set_state("line1\nline2ˇ");
31601 cx.update_editor(|e, window, cx| {
31602 e.set_mode(EditorMode::SingleLine);
31603 assert!(!e.key_context(window, cx).contains("start_of_input"));
31604 assert!(e.key_context(window, cx).contains("end_of_input"));
31605 });
31606 cx.set_state("ˇline1\nline2");
31607 cx.update_editor(|e, window, cx| {
31608 e.set_mode(EditorMode::SingleLine);
31609 assert!(e.key_context(window, cx).contains("start_of_input"));
31610 assert!(!e.key_context(window, cx).contains("end_of_input"));
31611 });
31612 cx.set_state("line1ˇ\nline2");
31613 cx.update_editor(|e, window, cx| {
31614 e.set_mode(EditorMode::SingleLine);
31615 assert!(!e.key_context(window, cx).contains("start_of_input"));
31616 assert!(!e.key_context(window, cx).contains("end_of_input"));
31617 });
31618
31619 cx.set_state("line1\nline2ˇ");
31620 cx.update_editor(|e, window, cx| {
31621 e.set_mode(EditorMode::AutoHeight {
31622 min_lines: 1,
31623 max_lines: Some(4),
31624 });
31625 assert!(!e.key_context(window, cx).contains("start_of_input"));
31626 assert!(e.key_context(window, cx).contains("end_of_input"));
31627 });
31628 cx.set_state("ˇline1\nline2");
31629 cx.update_editor(|e, window, cx| {
31630 e.set_mode(EditorMode::AutoHeight {
31631 min_lines: 1,
31632 max_lines: Some(4),
31633 });
31634 assert!(e.key_context(window, cx).contains("start_of_input"));
31635 assert!(!e.key_context(window, cx).contains("end_of_input"));
31636 });
31637 cx.set_state("line1ˇ\nline2");
31638 cx.update_editor(|e, window, cx| {
31639 e.set_mode(EditorMode::AutoHeight {
31640 min_lines: 1,
31641 max_lines: Some(4),
31642 });
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
31648#[gpui::test]
31649async fn test_sticky_scroll(cx: &mut TestAppContext) {
31650 init_test(cx, |_| {});
31651 let mut cx = EditorTestContext::new(cx).await;
31652
31653 let buffer = indoc! {"
31654 ˇfn foo() {
31655 let abc = 123;
31656 }
31657 struct Bar;
31658 impl Bar {
31659 fn new() -> Self {
31660 Self
31661 }
31662 }
31663 fn baz() {
31664 }
31665 "};
31666 cx.set_state(&buffer);
31667
31668 cx.update_editor(|e, _, cx| {
31669 e.buffer()
31670 .read(cx)
31671 .as_singleton()
31672 .unwrap()
31673 .update(cx, |buffer, cx| {
31674 buffer.set_language(Some(rust_lang()), cx);
31675 })
31676 });
31677
31678 let mut sticky_headers = |offset: ScrollOffset| {
31679 cx.update_editor(|e, window, cx| {
31680 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31681 });
31682 cx.run_until_parked();
31683 cx.update_editor(|e, window, cx| {
31684 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31685 .into_iter()
31686 .map(
31687 |StickyHeader {
31688 start_point,
31689 offset,
31690 ..
31691 }| { (start_point, offset) },
31692 )
31693 .collect::<Vec<_>>()
31694 })
31695 };
31696
31697 let fn_foo = Point { row: 0, column: 0 };
31698 let impl_bar = Point { row: 4, column: 0 };
31699 let fn_new = Point { row: 5, column: 4 };
31700
31701 assert_eq!(sticky_headers(0.0), vec![]);
31702 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
31703 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
31704 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
31705 assert_eq!(sticky_headers(2.0), vec![]);
31706 assert_eq!(sticky_headers(2.5), vec![]);
31707 assert_eq!(sticky_headers(3.0), vec![]);
31708 assert_eq!(sticky_headers(3.5), vec![]);
31709 assert_eq!(sticky_headers(4.0), vec![]);
31710 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
31711 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
31712 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
31713 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
31714 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
31715 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
31716 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
31717 assert_eq!(sticky_headers(8.0), vec![]);
31718 assert_eq!(sticky_headers(8.5), vec![]);
31719 assert_eq!(sticky_headers(9.0), vec![]);
31720 assert_eq!(sticky_headers(9.5), vec![]);
31721 assert_eq!(sticky_headers(10.0), vec![]);
31722}
31723
31724#[gpui::test]
31725async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
31726 executor: BackgroundExecutor,
31727 cx: &mut TestAppContext,
31728) {
31729 init_test(cx, |_| {});
31730 let mut cx = EditorTestContext::new(cx).await;
31731
31732 let diff_base = indoc! {"
31733 fn foo() {
31734 let a = 1;
31735 let b = 2;
31736 let c = 3;
31737 let d = 4;
31738 let e = 5;
31739 }
31740 "};
31741
31742 let buffer = indoc! {"
31743 ˇfn foo() {
31744 }
31745 "};
31746
31747 cx.set_state(&buffer);
31748
31749 cx.update_editor(|e, _, cx| {
31750 e.buffer()
31751 .read(cx)
31752 .as_singleton()
31753 .unwrap()
31754 .update(cx, |buffer, cx| {
31755 buffer.set_language(Some(rust_lang()), cx);
31756 })
31757 });
31758
31759 cx.set_head_text(diff_base);
31760 executor.run_until_parked();
31761
31762 cx.update_editor(|editor, window, cx| {
31763 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
31764 });
31765 executor.run_until_parked();
31766
31767 // After expanding, the display should look like:
31768 // row 0: fn foo() {
31769 // row 1: - let a = 1; (deleted)
31770 // row 2: - let b = 2; (deleted)
31771 // row 3: - let c = 3; (deleted)
31772 // row 4: - let d = 4; (deleted)
31773 // row 5: - let e = 5; (deleted)
31774 // row 6: }
31775 //
31776 // fn foo() spans display rows 0-6. Scrolling into the deleted region
31777 // (rows 1-5) should still show fn foo() as a sticky header.
31778
31779 let fn_foo = Point { row: 0, column: 0 };
31780
31781 let mut sticky_headers = |offset: ScrollOffset| {
31782 cx.update_editor(|e, window, cx| {
31783 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31784 });
31785 cx.run_until_parked();
31786 cx.update_editor(|e, window, cx| {
31787 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31788 .into_iter()
31789 .map(
31790 |StickyHeader {
31791 start_point,
31792 offset,
31793 ..
31794 }| { (start_point, offset) },
31795 )
31796 .collect::<Vec<_>>()
31797 })
31798 };
31799
31800 assert_eq!(sticky_headers(0.0), vec![]);
31801 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
31802 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
31803 // Scrolling into deleted lines: fn foo() should still be a sticky header.
31804 assert_eq!(sticky_headers(2.0), vec![(fn_foo, 0.0)]);
31805 assert_eq!(sticky_headers(3.0), vec![(fn_foo, 0.0)]);
31806 assert_eq!(sticky_headers(4.0), vec![(fn_foo, 0.0)]);
31807 assert_eq!(sticky_headers(5.0), vec![(fn_foo, 0.0)]);
31808 assert_eq!(sticky_headers(5.5), vec![(fn_foo, -0.5)]);
31809 // Past the closing brace: no more sticky header.
31810 assert_eq!(sticky_headers(6.0), vec![]);
31811}
31812
31813#[gpui::test]
31814async fn test_no_duplicated_sticky_headers(cx: &mut TestAppContext) {
31815 init_test(cx, |_| {});
31816 let mut cx = EditorTestContext::new(cx).await;
31817
31818 cx.set_state(indoc! {"
31819 ˇimpl Foo { fn bar() {
31820 let x = 1;
31821 fn baz() {
31822 let y = 2;
31823 }
31824 } }
31825 "});
31826
31827 cx.update_editor(|e, _, cx| {
31828 e.buffer()
31829 .read(cx)
31830 .as_singleton()
31831 .unwrap()
31832 .update(cx, |buffer, cx| {
31833 buffer.set_language(Some(rust_lang()), cx);
31834 })
31835 });
31836
31837 let mut sticky_headers = |offset: ScrollOffset| {
31838 cx.update_editor(|e, window, cx| {
31839 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
31840 });
31841 cx.run_until_parked();
31842 cx.update_editor(|e, window, cx| {
31843 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
31844 .into_iter()
31845 .map(
31846 |StickyHeader {
31847 start_point,
31848 offset,
31849 ..
31850 }| { (start_point, offset) },
31851 )
31852 .collect::<Vec<_>>()
31853 })
31854 };
31855
31856 let struct_foo = Point { row: 0, column: 0 };
31857 let fn_baz = Point { row: 2, column: 4 };
31858
31859 assert_eq!(sticky_headers(0.0), vec![]);
31860 assert_eq!(sticky_headers(0.5), vec![(struct_foo, 0.0)]);
31861 assert_eq!(sticky_headers(1.0), vec![(struct_foo, 0.0)]);
31862 assert_eq!(sticky_headers(1.5), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
31863 assert_eq!(sticky_headers(2.0), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
31864 assert_eq!(sticky_headers(2.5), vec![(struct_foo, 0.0), (fn_baz, 0.5)]);
31865 assert_eq!(sticky_headers(3.0), vec![(struct_foo, 0.0)]);
31866 assert_eq!(sticky_headers(3.5), vec![(struct_foo, 0.0)]);
31867 assert_eq!(sticky_headers(4.0), vec![(struct_foo, 0.0)]);
31868 assert_eq!(sticky_headers(4.5), vec![(struct_foo, -0.5)]);
31869 assert_eq!(sticky_headers(5.0), vec![]);
31870}
31871
31872#[gpui::test]
31873fn test_relative_line_numbers(cx: &mut TestAppContext) {
31874 init_test(cx, |_| {});
31875
31876 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
31877 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
31878 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
31879
31880 let multibuffer = cx.new(|cx| {
31881 let mut multibuffer = MultiBuffer::new(ReadWrite);
31882 multibuffer.set_excerpts_for_path(
31883 PathKey::sorted(0),
31884 buffer_1.clone(),
31885 [Point::new(0, 0)..Point::new(2, 0)],
31886 0,
31887 cx,
31888 );
31889 multibuffer.set_excerpts_for_path(
31890 PathKey::sorted(1),
31891 buffer_2.clone(),
31892 [Point::new(0, 0)..Point::new(2, 0)],
31893 0,
31894 cx,
31895 );
31896 multibuffer.set_excerpts_for_path(
31897 PathKey::sorted(2),
31898 buffer_3.clone(),
31899 [Point::new(0, 0)..Point::new(2, 0)],
31900 0,
31901 cx,
31902 );
31903 multibuffer
31904 });
31905
31906 // wrapped contents of multibuffer:
31907 // aaa
31908 // aaa
31909 // aaa
31910 // a
31911 // bbb
31912 //
31913 // ccc
31914 // ccc
31915 // ccc
31916 // c
31917 // ddd
31918 //
31919 // eee
31920 // fff
31921 // fff
31922 // fff
31923 // f
31924
31925 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
31926 _ = editor.update(cx, |editor, window, cx| {
31927 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
31928
31929 // includes trailing newlines.
31930 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
31931 let expected_wrapped_line_numbers = [
31932 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
31933 ];
31934
31935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31936 s.select_ranges([
31937 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
31938 ]);
31939 });
31940
31941 let snapshot = editor.snapshot(window, cx);
31942
31943 // these are all 0-indexed
31944 let base_display_row = DisplayRow(11);
31945 let base_row = 3;
31946 let wrapped_base_row = 7;
31947
31948 // test not counting wrapped lines
31949 let expected_relative_numbers = expected_line_numbers
31950 .into_iter()
31951 .enumerate()
31952 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
31953 .filter(|(_, relative_line_number)| *relative_line_number != 0)
31954 .collect_vec();
31955 let actual_relative_numbers = snapshot
31956 .calculate_relative_line_numbers(
31957 &(DisplayRow(0)..DisplayRow(24)),
31958 base_display_row,
31959 false,
31960 )
31961 .into_iter()
31962 .sorted()
31963 .collect_vec();
31964 assert_eq!(expected_relative_numbers, actual_relative_numbers);
31965 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
31966 for (display_row, relative_number) in expected_relative_numbers {
31967 assert_eq!(
31968 relative_number,
31969 snapshot
31970 .relative_line_delta(display_row, base_display_row, false)
31971 .unsigned_abs() as u32,
31972 );
31973 }
31974
31975 // test counting wrapped lines
31976 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
31977 .into_iter()
31978 .enumerate()
31979 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
31980 .filter(|(row, _)| *row != base_display_row)
31981 .collect_vec();
31982 let actual_relative_numbers = snapshot
31983 .calculate_relative_line_numbers(
31984 &(DisplayRow(0)..DisplayRow(24)),
31985 base_display_row,
31986 true,
31987 )
31988 .into_iter()
31989 .sorted()
31990 .collect_vec();
31991 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
31992 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
31993 for (display_row, relative_number) in expected_wrapped_relative_numbers {
31994 assert_eq!(
31995 relative_number,
31996 snapshot
31997 .relative_line_delta(display_row, base_display_row, true)
31998 .unsigned_abs() as u32,
31999 );
32000 }
32001 });
32002}
32003
32004#[gpui::test]
32005async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
32006 init_test(cx, |_| {});
32007 cx.update(|cx| {
32008 SettingsStore::update_global(cx, |store, cx| {
32009 store.update_user_settings(cx, |settings| {
32010 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
32011 enabled: Some(true),
32012 })
32013 });
32014 });
32015 });
32016 let mut cx = EditorTestContext::new(cx).await;
32017
32018 let line_height = cx.update_editor(|editor, window, cx| {
32019 editor
32020 .style(cx)
32021 .text
32022 .line_height_in_pixels(window.rem_size())
32023 });
32024
32025 let buffer = indoc! {"
32026 ˇfn foo() {
32027 let abc = 123;
32028 }
32029 struct Bar;
32030 impl Bar {
32031 fn new() -> Self {
32032 Self
32033 }
32034 }
32035 fn baz() {
32036 }
32037 "};
32038 cx.set_state(&buffer);
32039
32040 cx.update_editor(|e, _, cx| {
32041 e.buffer()
32042 .read(cx)
32043 .as_singleton()
32044 .unwrap()
32045 .update(cx, |buffer, cx| {
32046 buffer.set_language(Some(rust_lang()), cx);
32047 })
32048 });
32049
32050 let fn_foo = || empty_range(0, 0);
32051 let impl_bar = || empty_range(4, 0);
32052 let fn_new = || empty_range(5, 0);
32053
32054 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
32055 cx.update_editor(|e, window, cx| {
32056 e.scroll(
32057 gpui::Point {
32058 x: 0.,
32059 y: scroll_offset,
32060 },
32061 None,
32062 window,
32063 cx,
32064 );
32065 });
32066 cx.run_until_parked();
32067 cx.simulate_click(
32068 gpui::Point {
32069 x: px(0.),
32070 y: click_offset as f32 * line_height,
32071 },
32072 Modifiers::none(),
32073 );
32074 cx.run_until_parked();
32075 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
32076 };
32077 assert_eq!(
32078 scroll_and_click(
32079 4.5, // impl Bar is halfway off the screen
32080 0.0 // click top of screen
32081 ),
32082 // scrolled to impl Bar
32083 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32084 );
32085
32086 assert_eq!(
32087 scroll_and_click(
32088 4.5, // impl Bar is halfway off the screen
32089 0.25 // click middle of impl Bar
32090 ),
32091 // scrolled to impl Bar
32092 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32093 );
32094
32095 assert_eq!(
32096 scroll_and_click(
32097 4.5, // impl Bar is halfway off the screen
32098 1.5 // click below impl Bar (e.g. fn new())
32099 ),
32100 // scrolled to fn new() - this is below the impl Bar header which has persisted
32101 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
32102 );
32103
32104 assert_eq!(
32105 scroll_and_click(
32106 5.5, // fn new is halfway underneath impl Bar
32107 0.75 // click on the overlap of impl Bar and fn new()
32108 ),
32109 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
32110 );
32111
32112 assert_eq!(
32113 scroll_and_click(
32114 5.5, // fn new is halfway underneath impl Bar
32115 1.25 // click on the visible part of fn new()
32116 ),
32117 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
32118 );
32119
32120 assert_eq!(
32121 scroll_and_click(
32122 1.5, // fn foo is halfway off the screen
32123 0.0 // click top of screen
32124 ),
32125 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
32126 );
32127
32128 assert_eq!(
32129 scroll_and_click(
32130 1.5, // fn foo is halfway off the screen
32131 0.75 // click visible part of let abc...
32132 )
32133 .0,
32134 // no change in scroll
32135 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
32136 (gpui::Point { x: 0., y: 1.5 })
32137 );
32138
32139 // Verify clicking at a specific x position within a sticky header places
32140 // the cursor at the corresponding column.
32141 let (text_origin_x, em_width) = cx.update_editor(|editor, _, _| {
32142 let position_map = editor.last_position_map.as_ref().unwrap();
32143 (
32144 position_map.text_hitbox.bounds.origin.x,
32145 position_map.em_layout_width,
32146 )
32147 });
32148
32149 // Click on "impl Bar {" sticky header at column 5 (the 'B' in 'Bar').
32150 // The text "impl Bar {" starts at column 0, so column 5 = 'B'.
32151 let click_x = text_origin_x + em_width * 5.5;
32152 cx.update_editor(|e, window, cx| {
32153 e.scroll(gpui::Point { x: 0., y: 4.5 }, None, window, cx);
32154 });
32155 cx.run_until_parked();
32156 cx.simulate_click(
32157 gpui::Point {
32158 x: click_x,
32159 y: 0.25 * line_height,
32160 },
32161 Modifiers::none(),
32162 );
32163 cx.run_until_parked();
32164 let (scroll_pos, selections) =
32165 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)));
32166 assert_eq!(scroll_pos, gpui::Point { x: 0., y: 4. });
32167 assert_eq!(selections, vec![empty_range(4, 5)]);
32168}
32169
32170#[gpui::test]
32171async fn test_clicking_sticky_header_sets_character_select_mode(cx: &mut TestAppContext) {
32172 init_test(cx, |_| {});
32173 cx.update(|cx| {
32174 SettingsStore::update_global(cx, |store, cx| {
32175 store.update_user_settings(cx, |settings| {
32176 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
32177 enabled: Some(true),
32178 })
32179 });
32180 });
32181 });
32182 let mut cx = EditorTestContext::new(cx).await;
32183
32184 let line_height = cx.update_editor(|editor, window, cx| {
32185 editor
32186 .style(cx)
32187 .text
32188 .line_height_in_pixels(window.rem_size())
32189 });
32190
32191 let buffer = indoc! {"
32192 fn foo() {
32193 let abc = 123;
32194 }
32195 ˇstruct Bar;
32196 "};
32197 cx.set_state(&buffer);
32198
32199 cx.update_editor(|editor, _, cx| {
32200 editor
32201 .buffer()
32202 .read(cx)
32203 .as_singleton()
32204 .unwrap()
32205 .update(cx, |buffer, cx| {
32206 buffer.set_language(Some(rust_lang()), cx);
32207 })
32208 });
32209
32210 let text_origin_x = cx.update_editor(|editor, _, _| {
32211 editor
32212 .last_position_map
32213 .as_ref()
32214 .unwrap()
32215 .text_hitbox
32216 .bounds
32217 .origin
32218 .x
32219 });
32220
32221 cx.update_editor(|editor, window, cx| {
32222 // Double click on `struct` to select it
32223 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 1), false, 2, window, cx);
32224 editor.end_selection(window, cx);
32225
32226 // Scroll down one row to make `fn foo() {` a sticky header
32227 editor.scroll(gpui::Point { x: 0., y: 1. }, None, window, cx);
32228 });
32229 cx.run_until_parked();
32230
32231 // Click at the start of the `fn foo() {` sticky header
32232 cx.simulate_click(
32233 gpui::Point {
32234 x: text_origin_x,
32235 y: 0.5 * line_height,
32236 },
32237 Modifiers::none(),
32238 );
32239 cx.run_until_parked();
32240
32241 // Shift-click at the end of `fn foo() {` to select the whole row
32242 cx.update_editor(|editor, window, cx| {
32243 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
32244 editor.end_selection(window, cx);
32245 });
32246 cx.run_until_parked();
32247
32248 let selections = cx.update_editor(|editor, _, cx| display_ranges(editor, cx));
32249 assert_eq!(
32250 selections,
32251 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 10)]
32252 );
32253}
32254
32255#[gpui::test]
32256async fn test_next_prev_reference(cx: &mut TestAppContext) {
32257 const CYCLE_POSITIONS: &[&'static str] = &[
32258 indoc! {"
32259 fn foo() {
32260 let ˇabc = 123;
32261 let x = abc + 1;
32262 let y = abc + 2;
32263 let z = abc + 2;
32264 }
32265 "},
32266 indoc! {"
32267 fn foo() {
32268 let abc = 123;
32269 let x = ˇabc + 1;
32270 let y = abc + 2;
32271 let z = abc + 2;
32272 }
32273 "},
32274 indoc! {"
32275 fn foo() {
32276 let abc = 123;
32277 let x = abc + 1;
32278 let y = ˇabc + 2;
32279 let z = abc + 2;
32280 }
32281 "},
32282 indoc! {"
32283 fn foo() {
32284 let abc = 123;
32285 let x = abc + 1;
32286 let y = abc + 2;
32287 let z = ˇabc + 2;
32288 }
32289 "},
32290 ];
32291
32292 init_test(cx, |_| {});
32293
32294 let mut cx = EditorLspTestContext::new_rust(
32295 lsp::ServerCapabilities {
32296 references_provider: Some(lsp::OneOf::Left(true)),
32297 ..Default::default()
32298 },
32299 cx,
32300 )
32301 .await;
32302
32303 // importantly, the cursor is in the middle
32304 cx.set_state(indoc! {"
32305 fn foo() {
32306 let aˇbc = 123;
32307 let x = abc + 1;
32308 let y = abc + 2;
32309 let z = abc + 2;
32310 }
32311 "});
32312
32313 let reference_ranges = [
32314 lsp::Position::new(1, 8),
32315 lsp::Position::new(2, 12),
32316 lsp::Position::new(3, 12),
32317 lsp::Position::new(4, 12),
32318 ]
32319 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
32320
32321 cx.lsp
32322 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
32323 Ok(Some(
32324 reference_ranges
32325 .map(|range| lsp::Location {
32326 uri: params.text_document_position.text_document.uri.clone(),
32327 range,
32328 })
32329 .to_vec(),
32330 ))
32331 });
32332
32333 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
32334 cx.update_editor(|editor, window, cx| {
32335 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
32336 })
32337 .unwrap()
32338 .await
32339 .unwrap()
32340 };
32341
32342 _move(Direction::Next, 1, &mut cx).await;
32343 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32344
32345 _move(Direction::Next, 1, &mut cx).await;
32346 cx.assert_editor_state(CYCLE_POSITIONS[2]);
32347
32348 _move(Direction::Next, 1, &mut cx).await;
32349 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32350
32351 // loops back to the start
32352 _move(Direction::Next, 1, &mut cx).await;
32353 cx.assert_editor_state(CYCLE_POSITIONS[0]);
32354
32355 // loops back to the end
32356 _move(Direction::Prev, 1, &mut cx).await;
32357 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32358
32359 _move(Direction::Prev, 1, &mut cx).await;
32360 cx.assert_editor_state(CYCLE_POSITIONS[2]);
32361
32362 _move(Direction::Prev, 1, &mut cx).await;
32363 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32364
32365 _move(Direction::Prev, 1, &mut cx).await;
32366 cx.assert_editor_state(CYCLE_POSITIONS[0]);
32367
32368 _move(Direction::Next, 3, &mut cx).await;
32369 cx.assert_editor_state(CYCLE_POSITIONS[3]);
32370
32371 _move(Direction::Prev, 2, &mut cx).await;
32372 cx.assert_editor_state(CYCLE_POSITIONS[1]);
32373}
32374
32375#[gpui::test]
32376async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
32377 init_test(cx, |_| {});
32378
32379 let (editor, cx) = cx.add_window_view(|window, cx| {
32380 let multi_buffer = MultiBuffer::build_multi(
32381 [
32382 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32383 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32384 ],
32385 cx,
32386 );
32387 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32388 });
32389
32390 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
32391 let buffer_ids = cx.multibuffer(|mb, cx| {
32392 mb.snapshot(cx)
32393 .excerpts()
32394 .map(|excerpt| excerpt.context.start.buffer_id)
32395 .collect::<Vec<_>>()
32396 });
32397
32398 cx.assert_excerpts_with_selections(indoc! {"
32399 [EXCERPT]
32400 ˇ1
32401 2
32402 3
32403 [EXCERPT]
32404 1
32405 2
32406 3
32407 "});
32408
32409 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
32410 cx.update_editor(|editor, window, cx| {
32411 editor.change_selections(None.into(), window, cx, |s| {
32412 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32413 });
32414 });
32415 cx.assert_excerpts_with_selections(indoc! {"
32416 [EXCERPT]
32417 1
32418 2ˇ
32419 3
32420 [EXCERPT]
32421 1
32422 2
32423 3
32424 "});
32425
32426 cx.update_editor(|editor, window, cx| {
32427 editor
32428 .select_all_matches(&SelectAllMatches, window, cx)
32429 .unwrap();
32430 });
32431 cx.assert_excerpts_with_selections(indoc! {"
32432 [EXCERPT]
32433 1
32434 2ˇ
32435 3
32436 [EXCERPT]
32437 1
32438 2ˇ
32439 3
32440 "});
32441
32442 cx.update_editor(|editor, window, cx| {
32443 editor.handle_input("X", window, cx);
32444 });
32445 cx.assert_excerpts_with_selections(indoc! {"
32446 [EXCERPT]
32447 1
32448 Xˇ
32449 3
32450 [EXCERPT]
32451 1
32452 Xˇ
32453 3
32454 "});
32455
32456 // Scenario 2: Select "2", then fold second buffer before insertion
32457 cx.update_multibuffer(|mb, cx| {
32458 for buffer_id in buffer_ids.iter() {
32459 let buffer = mb.buffer(*buffer_id).unwrap();
32460 buffer.update(cx, |buffer, cx| {
32461 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
32462 });
32463 }
32464 });
32465
32466 // Select "2" and select all matches
32467 cx.update_editor(|editor, window, cx| {
32468 editor.change_selections(None.into(), window, cx, |s| {
32469 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32470 });
32471 editor
32472 .select_all_matches(&SelectAllMatches, window, cx)
32473 .unwrap();
32474 });
32475
32476 // Fold second buffer - should remove selections from folded buffer
32477 cx.update_editor(|editor, _, cx| {
32478 editor.fold_buffer(buffer_ids[1], cx);
32479 });
32480 cx.assert_excerpts_with_selections(indoc! {"
32481 [EXCERPT]
32482 1
32483 2ˇ
32484 3
32485 [EXCERPT]
32486 [FOLDED]
32487 "});
32488
32489 // Insert text - should only affect first buffer
32490 cx.update_editor(|editor, window, cx| {
32491 editor.handle_input("Y", window, cx);
32492 });
32493 cx.update_editor(|editor, _, cx| {
32494 editor.unfold_buffer(buffer_ids[1], cx);
32495 });
32496 cx.assert_excerpts_with_selections(indoc! {"
32497 [EXCERPT]
32498 1
32499 Yˇ
32500 3
32501 [EXCERPT]
32502 1
32503 2
32504 3
32505 "});
32506
32507 // Scenario 3: Select "2", then fold first buffer before insertion
32508 cx.update_multibuffer(|mb, cx| {
32509 for buffer_id in buffer_ids.iter() {
32510 let buffer = mb.buffer(*buffer_id).unwrap();
32511 buffer.update(cx, |buffer, cx| {
32512 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
32513 });
32514 }
32515 });
32516
32517 // Select "2" and select all matches
32518 cx.update_editor(|editor, window, cx| {
32519 editor.change_selections(None.into(), window, cx, |s| {
32520 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
32521 });
32522 editor
32523 .select_all_matches(&SelectAllMatches, window, cx)
32524 .unwrap();
32525 });
32526
32527 // Fold first buffer - should remove selections from folded buffer
32528 cx.update_editor(|editor, _, cx| {
32529 editor.fold_buffer(buffer_ids[0], cx);
32530 });
32531 cx.assert_excerpts_with_selections(indoc! {"
32532 [EXCERPT]
32533 [FOLDED]
32534 [EXCERPT]
32535 1
32536 2ˇ
32537 3
32538 "});
32539
32540 // Insert text - should only affect second buffer
32541 cx.update_editor(|editor, window, cx| {
32542 editor.handle_input("Z", window, cx);
32543 });
32544 cx.update_editor(|editor, _, cx| {
32545 editor.unfold_buffer(buffer_ids[0], cx);
32546 });
32547 cx.assert_excerpts_with_selections(indoc! {"
32548 [EXCERPT]
32549 1
32550 2
32551 3
32552 [EXCERPT]
32553 1
32554 Zˇ
32555 3
32556 "});
32557
32558 // Test correct folded header is selected upon fold
32559 cx.update_editor(|editor, _, cx| {
32560 editor.fold_buffer(buffer_ids[0], cx);
32561 editor.fold_buffer(buffer_ids[1], cx);
32562 });
32563 cx.assert_excerpts_with_selections(indoc! {"
32564 [EXCERPT]
32565 [FOLDED]
32566 [EXCERPT]
32567 ˇ[FOLDED]
32568 "});
32569
32570 // Test selection inside folded buffer unfolds it on type
32571 cx.update_editor(|editor, window, cx| {
32572 editor.handle_input("W", window, cx);
32573 });
32574 cx.update_editor(|editor, _, cx| {
32575 editor.unfold_buffer(buffer_ids[0], cx);
32576 });
32577 cx.assert_excerpts_with_selections(indoc! {"
32578 [EXCERPT]
32579 1
32580 2
32581 3
32582 [EXCERPT]
32583 Wˇ1
32584 Z
32585 3
32586 "});
32587}
32588
32589#[gpui::test]
32590async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
32591 init_test(cx, |_| {});
32592
32593 let (editor, cx) = cx.add_window_view(|window, cx| {
32594 let multi_buffer = MultiBuffer::build_multi(
32595 [
32596 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
32597 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
32598 ],
32599 cx,
32600 );
32601 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32602 });
32603
32604 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
32605
32606 cx.assert_excerpts_with_selections(indoc! {"
32607 [EXCERPT]
32608 ˇ1
32609 2
32610 3
32611 [EXCERPT]
32612 1
32613 2
32614 3
32615 4
32616 5
32617 6
32618 7
32619 8
32620 9
32621 "});
32622
32623 cx.update_editor(|editor, window, cx| {
32624 editor.change_selections(None.into(), window, cx, |s| {
32625 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
32626 });
32627 });
32628
32629 cx.assert_excerpts_with_selections(indoc! {"
32630 [EXCERPT]
32631 1
32632 2
32633 3
32634 [EXCERPT]
32635 1
32636 2
32637 3
32638 4
32639 5
32640 6
32641 ˇ7
32642 8
32643 9
32644 "});
32645
32646 cx.update_editor(|editor, _window, cx| {
32647 editor.set_vertical_scroll_margin(0, cx);
32648 });
32649
32650 cx.update_editor(|editor, window, cx| {
32651 assert_eq!(editor.vertical_scroll_margin(), 0);
32652 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
32653 assert_eq!(
32654 editor.snapshot(window, cx).scroll_position(),
32655 gpui::Point::new(0., 12.0)
32656 );
32657 });
32658
32659 cx.update_editor(|editor, _window, cx| {
32660 editor.set_vertical_scroll_margin(3, cx);
32661 });
32662
32663 cx.update_editor(|editor, window, cx| {
32664 assert_eq!(editor.vertical_scroll_margin(), 3);
32665 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
32666 assert_eq!(
32667 editor.snapshot(window, cx).scroll_position(),
32668 gpui::Point::new(0., 9.0)
32669 );
32670 });
32671}
32672
32673#[gpui::test]
32674async fn test_find_references_single_case(cx: &mut TestAppContext) {
32675 init_test(cx, |_| {});
32676 let mut cx = EditorLspTestContext::new_rust(
32677 lsp::ServerCapabilities {
32678 references_provider: Some(lsp::OneOf::Left(true)),
32679 ..lsp::ServerCapabilities::default()
32680 },
32681 cx,
32682 )
32683 .await;
32684
32685 let before = indoc!(
32686 r#"
32687 fn main() {
32688 let aˇbc = 123;
32689 let xyz = abc;
32690 }
32691 "#
32692 );
32693 let after = indoc!(
32694 r#"
32695 fn main() {
32696 let abc = 123;
32697 let xyz = ˇabc;
32698 }
32699 "#
32700 );
32701
32702 cx.lsp
32703 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
32704 Ok(Some(vec![
32705 lsp::Location {
32706 uri: params.text_document_position.text_document.uri.clone(),
32707 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
32708 },
32709 lsp::Location {
32710 uri: params.text_document_position.text_document.uri,
32711 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
32712 },
32713 ]))
32714 });
32715
32716 cx.set_state(before);
32717
32718 let action = FindAllReferences {
32719 always_open_multibuffer: false,
32720 };
32721
32722 let navigated = cx
32723 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
32724 .expect("should have spawned a task")
32725 .await
32726 .unwrap();
32727
32728 assert_eq!(navigated, Navigated::No);
32729
32730 cx.run_until_parked();
32731
32732 cx.assert_editor_state(after);
32733}
32734
32735#[gpui::test]
32736async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
32737 init_test(cx, |settings| {
32738 settings.defaults.tab_size = Some(2.try_into().unwrap());
32739 });
32740
32741 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
32742 let mut cx = EditorTestContext::new(cx).await;
32743 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
32744
32745 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
32746 cx.set_state(indoc! {"
32747 - [ ] taskˇ
32748 "});
32749 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32750 cx.wait_for_autoindent_applied().await;
32751 cx.assert_editor_state(indoc! {"
32752 - [ ] task
32753 - [ ] ˇ
32754 "});
32755
32756 // Case 2: Works with checked task items too
32757 cx.set_state(indoc! {"
32758 - [x] completed taskˇ
32759 "});
32760 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32761 cx.wait_for_autoindent_applied().await;
32762 cx.assert_editor_state(indoc! {"
32763 - [x] completed task
32764 - [ ] ˇ
32765 "});
32766
32767 // Case 2.1: Works with uppercase checked marker too
32768 cx.set_state(indoc! {"
32769 - [X] completed taskˇ
32770 "});
32771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32772 cx.wait_for_autoindent_applied().await;
32773 cx.assert_editor_state(indoc! {"
32774 - [X] completed task
32775 - [ ] ˇ
32776 "});
32777
32778 // Case 3: Cursor position doesn't matter - content after marker is what counts
32779 cx.set_state(indoc! {"
32780 - [ ] taˇsk
32781 "});
32782 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32783 cx.wait_for_autoindent_applied().await;
32784 cx.assert_editor_state(indoc! {"
32785 - [ ] ta
32786 - [ ] ˇsk
32787 "});
32788
32789 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
32790 cx.set_state(indoc! {"
32791 - [ ] ˇ
32792 "});
32793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32794 cx.wait_for_autoindent_applied().await;
32795 cx.assert_editor_state(
32796 indoc! {"
32797 - [ ]$$
32798 ˇ
32799 "}
32800 .replace("$", " ")
32801 .as_str(),
32802 );
32803
32804 // Case 5: Adding newline with content adds marker preserving indentation
32805 cx.set_state(indoc! {"
32806 - [ ] task
32807 - [ ] indentedˇ
32808 "});
32809 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32810 cx.wait_for_autoindent_applied().await;
32811 cx.assert_editor_state(indoc! {"
32812 - [ ] task
32813 - [ ] indented
32814 - [ ] ˇ
32815 "});
32816
32817 // Case 6: Adding newline with cursor right after prefix, unindents
32818 cx.set_state(indoc! {"
32819 - [ ] task
32820 - [ ] sub task
32821 - [ ] ˇ
32822 "});
32823 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32824 cx.wait_for_autoindent_applied().await;
32825 cx.assert_editor_state(indoc! {"
32826 - [ ] task
32827 - [ ] sub task
32828 - [ ] ˇ
32829 "});
32830 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32831 cx.wait_for_autoindent_applied().await;
32832
32833 // Case 7: Adding newline with cursor right after prefix, removes marker
32834 cx.assert_editor_state(indoc! {"
32835 - [ ] task
32836 - [ ] sub task
32837 - [ ] ˇ
32838 "});
32839 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32840 cx.wait_for_autoindent_applied().await;
32841 cx.assert_editor_state(indoc! {"
32842 - [ ] task
32843 - [ ] sub task
32844 ˇ
32845 "});
32846
32847 // Case 8: Cursor before or inside prefix does not add marker
32848 cx.set_state(indoc! {"
32849 ˇ- [ ] task
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
32855 ˇ- [ ] task
32856 "});
32857
32858 cx.set_state(indoc! {"
32859 - [ˇ ] task
32860 "});
32861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32862 cx.wait_for_autoindent_applied().await;
32863 cx.assert_editor_state(indoc! {"
32864 - [
32865 ˇ
32866 ] task
32867 "});
32868}
32869
32870#[gpui::test]
32871async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
32872 init_test(cx, |settings| {
32873 settings.defaults.tab_size = Some(2.try_into().unwrap());
32874 });
32875
32876 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
32877 let mut cx = EditorTestContext::new(cx).await;
32878 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
32879
32880 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
32881 cx.set_state(indoc! {"
32882 - itemˇ
32883 "});
32884 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32885 cx.wait_for_autoindent_applied().await;
32886 cx.assert_editor_state(indoc! {"
32887 - item
32888 - ˇ
32889 "});
32890
32891 // Case 2: Works with different markers
32892 cx.set_state(indoc! {"
32893 * starred itemˇ
32894 "});
32895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32896 cx.wait_for_autoindent_applied().await;
32897 cx.assert_editor_state(indoc! {"
32898 * starred item
32899 * ˇ
32900 "});
32901
32902 cx.set_state(indoc! {"
32903 + plus itemˇ
32904 "});
32905 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32906 cx.wait_for_autoindent_applied().await;
32907 cx.assert_editor_state(indoc! {"
32908 + plus item
32909 + ˇ
32910 "});
32911
32912 // Case 3: Cursor position doesn't matter - content after marker is what counts
32913 cx.set_state(indoc! {"
32914 - itˇem
32915 "});
32916 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32917 cx.wait_for_autoindent_applied().await;
32918 cx.assert_editor_state(indoc! {"
32919 - it
32920 - ˇem
32921 "});
32922
32923 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
32924 cx.set_state(indoc! {"
32925 - ˇ
32926 "});
32927 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32928 cx.wait_for_autoindent_applied().await;
32929 cx.assert_editor_state(
32930 indoc! {"
32931 - $
32932 ˇ
32933 "}
32934 .replace("$", " ")
32935 .as_str(),
32936 );
32937
32938 // Case 5: Adding newline with content adds marker preserving indentation
32939 cx.set_state(indoc! {"
32940 - item
32941 - indentedˇ
32942 "});
32943 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32944 cx.wait_for_autoindent_applied().await;
32945 cx.assert_editor_state(indoc! {"
32946 - item
32947 - indented
32948 - ˇ
32949 "});
32950
32951 // Case 6: Adding newline with cursor right after marker, unindents
32952 cx.set_state(indoc! {"
32953 - item
32954 - sub item
32955 - ˇ
32956 "});
32957 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32958 cx.wait_for_autoindent_applied().await;
32959 cx.assert_editor_state(indoc! {"
32960 - item
32961 - sub item
32962 - ˇ
32963 "});
32964 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32965 cx.wait_for_autoindent_applied().await;
32966
32967 // Case 7: Adding newline with cursor right after marker, removes marker
32968 cx.assert_editor_state(indoc! {"
32969 - item
32970 - sub item
32971 - ˇ
32972 "});
32973 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32974 cx.wait_for_autoindent_applied().await;
32975 cx.assert_editor_state(indoc! {"
32976 - item
32977 - sub item
32978 ˇ
32979 "});
32980
32981 // Case 8: Cursor before or inside prefix does not add marker
32982 cx.set_state(indoc! {"
32983 ˇ- item
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
32989 ˇ- item
32990 "});
32991
32992 cx.set_state(indoc! {"
32993 -ˇ item
32994 "});
32995 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
32996 cx.wait_for_autoindent_applied().await;
32997 cx.assert_editor_state(indoc! {"
32998 -
32999 ˇitem
33000 "});
33001}
33002
33003#[gpui::test]
33004async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
33005 init_test(cx, |settings| {
33006 settings.defaults.tab_size = Some(2.try_into().unwrap());
33007 });
33008
33009 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33010 let mut cx = EditorTestContext::new(cx).await;
33011 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33012
33013 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
33014 cx.set_state(indoc! {"
33015 1. first itemˇ
33016 "});
33017 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33018 cx.wait_for_autoindent_applied().await;
33019 cx.assert_editor_state(indoc! {"
33020 1. first item
33021 2. ˇ
33022 "});
33023
33024 // Case 2: Works with larger numbers
33025 cx.set_state(indoc! {"
33026 10. tenth itemˇ
33027 "});
33028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33029 cx.wait_for_autoindent_applied().await;
33030 cx.assert_editor_state(indoc! {"
33031 10. tenth item
33032 11. ˇ
33033 "});
33034
33035 // Case 3: Cursor position doesn't matter - content after marker is what counts
33036 cx.set_state(indoc! {"
33037 1. itˇem
33038 "});
33039 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33040 cx.wait_for_autoindent_applied().await;
33041 cx.assert_editor_state(indoc! {"
33042 1. it
33043 2. ˇem
33044 "});
33045
33046 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
33047 cx.set_state(indoc! {"
33048 1. ˇ
33049 "});
33050 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33051 cx.wait_for_autoindent_applied().await;
33052 cx.assert_editor_state(
33053 indoc! {"
33054 1. $
33055 ˇ
33056 "}
33057 .replace("$", " ")
33058 .as_str(),
33059 );
33060
33061 // Case 5: Adding newline with content adds marker preserving indentation
33062 cx.set_state(indoc! {"
33063 1. item
33064 2. indentedˇ
33065 "});
33066 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33067 cx.wait_for_autoindent_applied().await;
33068 cx.assert_editor_state(indoc! {"
33069 1. item
33070 2. indented
33071 3. ˇ
33072 "});
33073
33074 // Case 6: Adding newline with cursor right after marker, unindents
33075 cx.set_state(indoc! {"
33076 1. item
33077 2. sub item
33078 3. ˇ
33079 "});
33080 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33081 cx.wait_for_autoindent_applied().await;
33082 cx.assert_editor_state(indoc! {"
33083 1. item
33084 2. sub item
33085 1. ˇ
33086 "});
33087 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33088 cx.wait_for_autoindent_applied().await;
33089
33090 // Case 7: Adding newline with cursor right after marker, removes marker
33091 cx.assert_editor_state(indoc! {"
33092 1. item
33093 2. sub item
33094 1. ˇ
33095 "});
33096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33097 cx.wait_for_autoindent_applied().await;
33098 cx.assert_editor_state(indoc! {"
33099 1. item
33100 2. sub item
33101 ˇ
33102 "});
33103
33104 // Case 8: Cursor before or inside prefix does not add marker
33105 cx.set_state(indoc! {"
33106 ˇ1. item
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
33112 ˇ1. item
33113 "});
33114
33115 cx.set_state(indoc! {"
33116 1ˇ. item
33117 "});
33118 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33119 cx.wait_for_autoindent_applied().await;
33120 cx.assert_editor_state(indoc! {"
33121 1
33122 ˇ. item
33123 "});
33124}
33125
33126#[gpui::test]
33127async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
33128 init_test(cx, |settings| {
33129 settings.defaults.tab_size = Some(2.try_into().unwrap());
33130 });
33131
33132 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33133 let mut cx = EditorTestContext::new(cx).await;
33134 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33135
33136 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
33137 cx.set_state(indoc! {"
33138 1. first item
33139 1. sub first item
33140 2. sub second item
33141 3. ˇ
33142 "});
33143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
33144 cx.wait_for_autoindent_applied().await;
33145 cx.assert_editor_state(indoc! {"
33146 1. first item
33147 1. sub first item
33148 2. sub second item
33149 1. ˇ
33150 "});
33151}
33152
33153#[gpui::test]
33154async fn test_tab_list_indent(cx: &mut TestAppContext) {
33155 init_test(cx, |settings| {
33156 settings.defaults.tab_size = Some(2.try_into().unwrap());
33157 });
33158
33159 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
33160 let mut cx = EditorTestContext::new(cx).await;
33161 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
33162
33163 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
33164 cx.set_state(indoc! {"
33165 - ˇitem
33166 "});
33167 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33168 cx.wait_for_autoindent_applied().await;
33169 let expected = indoc! {"
33170 $$- ˇitem
33171 "};
33172 cx.assert_editor_state(expected.replace("$", " ").as_str());
33173
33174 // Case 2: Task list - cursor after prefix
33175 cx.set_state(indoc! {"
33176 - [ ] ˇtask
33177 "});
33178 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33179 cx.wait_for_autoindent_applied().await;
33180 let expected = indoc! {"
33181 $$- [ ] ˇtask
33182 "};
33183 cx.assert_editor_state(expected.replace("$", " ").as_str());
33184
33185 // Case 3: Ordered list - cursor after prefix
33186 cx.set_state(indoc! {"
33187 1. ˇfirst
33188 "});
33189 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33190 cx.wait_for_autoindent_applied().await;
33191 let expected = indoc! {"
33192 $$1. ˇfirst
33193 "};
33194 cx.assert_editor_state(expected.replace("$", " ").as_str());
33195
33196 // Case 4: With existing indentation - adds more indent
33197 let initial = indoc! {"
33198 $$- ˇitem
33199 "};
33200 cx.set_state(initial.replace("$", " ").as_str());
33201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33202 cx.wait_for_autoindent_applied().await;
33203 let expected = indoc! {"
33204 $$$$- ˇitem
33205 "};
33206 cx.assert_editor_state(expected.replace("$", " ").as_str());
33207
33208 // Case 5: Empty list item
33209 cx.set_state(indoc! {"
33210 - ˇ
33211 "});
33212 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33213 cx.wait_for_autoindent_applied().await;
33214 let expected = indoc! {"
33215 $$- ˇ
33216 "};
33217 cx.assert_editor_state(expected.replace("$", " ").as_str());
33218
33219 // Case 6: Cursor at end of line with content
33220 cx.set_state(indoc! {"
33221 - itemˇ
33222 "});
33223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33224 cx.wait_for_autoindent_applied().await;
33225 let expected = indoc! {"
33226 $$- itemˇ
33227 "};
33228 cx.assert_editor_state(expected.replace("$", " ").as_str());
33229
33230 // Case 7: Cursor at start of list item, indents it
33231 cx.set_state(indoc! {"
33232 - item
33233 ˇ - sub item
33234 "});
33235 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33236 cx.wait_for_autoindent_applied().await;
33237 let expected = indoc! {"
33238 - item
33239 ˇ - sub item
33240 "};
33241 cx.assert_editor_state(expected);
33242
33243 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
33244 cx.update_editor(|_, _, cx| {
33245 SettingsStore::update_global(cx, |store, cx| {
33246 store.update_user_settings(cx, |settings| {
33247 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
33248 });
33249 });
33250 });
33251 cx.set_state(indoc! {"
33252 - item
33253 ˇ - sub item
33254 "});
33255 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
33256 cx.wait_for_autoindent_applied().await;
33257 let expected = indoc! {"
33258 - item
33259 ˇ- sub item
33260 "};
33261 cx.assert_editor_state(expected);
33262}
33263
33264#[gpui::test]
33265async fn test_local_worktree_trust(cx: &mut TestAppContext) {
33266 init_test(cx, |_| {});
33267 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
33268
33269 cx.update(|cx| {
33270 SettingsStore::update_global(cx, |store, cx| {
33271 store.update_user_settings(cx, |settings| {
33272 settings.project.all_languages.defaults.inlay_hints =
33273 Some(InlayHintSettingsContent {
33274 enabled: Some(true),
33275 ..InlayHintSettingsContent::default()
33276 });
33277 });
33278 });
33279 });
33280
33281 let fs = FakeFs::new(cx.executor());
33282 fs.insert_tree(
33283 path!("/project"),
33284 json!({
33285 ".zed": {
33286 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
33287 },
33288 "main.rs": "fn main() {}"
33289 }),
33290 )
33291 .await;
33292
33293 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
33294 let server_name = "override-rust-analyzer";
33295 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
33296
33297 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
33298 language_registry.add(rust_lang());
33299
33300 let capabilities = lsp::ServerCapabilities {
33301 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
33302 ..lsp::ServerCapabilities::default()
33303 };
33304 let mut fake_language_servers = language_registry.register_fake_lsp(
33305 "Rust",
33306 FakeLspAdapter {
33307 name: server_name,
33308 capabilities,
33309 initializer: Some(Box::new({
33310 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
33311 move |fake_server| {
33312 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
33313 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
33314 move |_params, _| {
33315 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
33316 async move {
33317 Ok(Some(vec![lsp::InlayHint {
33318 position: lsp::Position::new(0, 0),
33319 label: lsp::InlayHintLabel::String("hint".to_string()),
33320 kind: None,
33321 text_edits: None,
33322 tooltip: None,
33323 padding_left: None,
33324 padding_right: None,
33325 data: None,
33326 }]))
33327 }
33328 },
33329 );
33330 }
33331 })),
33332 ..FakeLspAdapter::default()
33333 },
33334 );
33335
33336 cx.run_until_parked();
33337
33338 let worktree_id = project.read_with(cx, |project, cx| {
33339 project
33340 .worktrees(cx)
33341 .next()
33342 .map(|wt| wt.read(cx).id())
33343 .expect("should have a worktree")
33344 });
33345 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
33346
33347 let trusted_worktrees =
33348 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
33349
33350 let can_trust = trusted_worktrees.update(cx, |store, cx| {
33351 store.can_trust(&worktree_store, worktree_id, cx)
33352 });
33353 assert!(!can_trust, "worktree should be restricted initially");
33354
33355 let buffer_before_approval = project
33356 .update(cx, |project, cx| {
33357 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
33358 })
33359 .await
33360 .unwrap();
33361
33362 let (editor, cx) = cx.add_window_view(|window, cx| {
33363 Editor::new(
33364 EditorMode::full(),
33365 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
33366 Some(project.clone()),
33367 window,
33368 cx,
33369 )
33370 });
33371 cx.run_until_parked();
33372 let fake_language_server = fake_language_servers.next();
33373
33374 cx.read(|cx| {
33375 assert_eq!(
33376 language::language_settings::LanguageSettings::for_buffer(
33377 buffer_before_approval.read(cx),
33378 cx
33379 )
33380 .language_servers,
33381 ["...".to_string()],
33382 "local .zed/settings.json must not apply before trust approval"
33383 )
33384 });
33385
33386 editor.update_in(cx, |editor, window, cx| {
33387 editor.handle_input("1", window, cx);
33388 });
33389 cx.run_until_parked();
33390 cx.executor()
33391 .advance_clock(std::time::Duration::from_secs(1));
33392 assert_eq!(
33393 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
33394 0,
33395 "inlay hints must not be queried before trust approval"
33396 );
33397
33398 trusted_worktrees.update(cx, |store, cx| {
33399 store.trust(
33400 &worktree_store,
33401 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
33402 cx,
33403 );
33404 });
33405 cx.run_until_parked();
33406
33407 cx.read(|cx| {
33408 assert_eq!(
33409 language::language_settings::LanguageSettings::for_buffer(
33410 buffer_before_approval.read(cx),
33411 cx
33412 )
33413 .language_servers,
33414 ["override-rust-analyzer".to_string()],
33415 "local .zed/settings.json should apply after trust approval"
33416 )
33417 });
33418 let _fake_language_server = fake_language_server.await.unwrap();
33419 editor.update_in(cx, |editor, window, cx| {
33420 editor.handle_input("1", window, cx);
33421 });
33422 cx.run_until_parked();
33423 cx.executor()
33424 .advance_clock(std::time::Duration::from_secs(1));
33425 assert!(
33426 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
33427 "inlay hints should be queried after trust approval"
33428 );
33429
33430 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
33431 store.can_trust(&worktree_store, worktree_id, cx)
33432 });
33433 assert!(can_trust_after, "worktree should be trusted after trust()");
33434}
33435
33436#[gpui::test]
33437fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
33438 // This test reproduces a bug where drawing an editor at a position above the viewport
33439 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
33440 // causes an infinite loop in blocks_in_range.
33441 //
33442 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
33443 // the content mask intersection produces visible_bounds with origin at the viewport top.
33444 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
33445 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
33446 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
33447 init_test(cx, |_| {});
33448
33449 let window = cx.add_window(|_, _| gpui::Empty);
33450 let mut cx = VisualTestContext::from_window(*window, cx);
33451
33452 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
33453 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
33454
33455 // Simulate a small viewport (500x500 pixels at origin 0,0)
33456 cx.simulate_resize(gpui::size(px(500.), px(500.)));
33457
33458 // Draw the editor at a very negative Y position, simulating an editor that's been
33459 // scrolled way above the visible viewport (like in a List that has scrolled past it).
33460 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
33461 // This should NOT hang - it should just render nothing.
33462 cx.draw(
33463 gpui::point(px(0.), px(-10000.)),
33464 gpui::size(px(500.), px(3000.)),
33465 |_, _| editor.clone().into_any_element(),
33466 );
33467
33468 // If we get here without hanging, the test passes
33469}
33470
33471#[gpui::test]
33472async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
33473 init_test(cx, |_| {});
33474
33475 let fs = FakeFs::new(cx.executor());
33476 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33477 .await;
33478
33479 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33480 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33481 let workspace = window
33482 .read_with(cx, |mw, _| mw.workspace().clone())
33483 .unwrap();
33484 let cx = &mut VisualTestContext::from_window(*window, cx);
33485
33486 let editor = workspace
33487 .update_in(cx, |workspace, window, cx| {
33488 workspace.open_abs_path(
33489 PathBuf::from(path!("/root/file.txt")),
33490 OpenOptions::default(),
33491 window,
33492 cx,
33493 )
33494 })
33495 .await
33496 .unwrap()
33497 .downcast::<Editor>()
33498 .unwrap();
33499
33500 // Enable diff review button mode
33501 editor.update(cx, |editor, cx| {
33502 editor.set_show_diff_review_button(true, cx);
33503 });
33504
33505 // Initially, no indicator should be present
33506 editor.update(cx, |editor, _cx| {
33507 assert!(
33508 editor.gutter_diff_review_indicator.0.is_none(),
33509 "Indicator should be None initially"
33510 );
33511 });
33512}
33513
33514#[gpui::test]
33515async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
33516 init_test(cx, |_| {});
33517
33518 // Register DisableAiSettings and set disable_ai to true
33519 cx.update(|cx| {
33520 project::DisableAiSettings::register(cx);
33521 project::DisableAiSettings::override_global(
33522 project::DisableAiSettings { disable_ai: true },
33523 cx,
33524 );
33525 });
33526
33527 let fs = FakeFs::new(cx.executor());
33528 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33529 .await;
33530
33531 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33532 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33533 let workspace = window
33534 .read_with(cx, |mw, _| mw.workspace().clone())
33535 .unwrap();
33536 let cx = &mut VisualTestContext::from_window(*window, cx);
33537
33538 let editor = workspace
33539 .update_in(cx, |workspace, window, cx| {
33540 workspace.open_abs_path(
33541 PathBuf::from(path!("/root/file.txt")),
33542 OpenOptions::default(),
33543 window,
33544 cx,
33545 )
33546 })
33547 .await
33548 .unwrap()
33549 .downcast::<Editor>()
33550 .unwrap();
33551
33552 // Enable diff review button mode
33553 editor.update(cx, |editor, cx| {
33554 editor.set_show_diff_review_button(true, cx);
33555 });
33556
33557 // Verify AI is disabled
33558 cx.read(|cx| {
33559 assert!(
33560 project::DisableAiSettings::get_global(cx).disable_ai,
33561 "AI should be disabled"
33562 );
33563 });
33564
33565 // The indicator should not be created when AI is disabled
33566 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
33567 editor.update(cx, |editor, _cx| {
33568 assert!(
33569 editor.gutter_diff_review_indicator.0.is_none(),
33570 "Indicator should be None when AI is disabled"
33571 );
33572 });
33573}
33574
33575#[gpui::test]
33576async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
33577 init_test(cx, |_| {});
33578
33579 // Register DisableAiSettings and set disable_ai to false
33580 cx.update(|cx| {
33581 project::DisableAiSettings::register(cx);
33582 project::DisableAiSettings::override_global(
33583 project::DisableAiSettings { disable_ai: false },
33584 cx,
33585 );
33586 });
33587
33588 let fs = FakeFs::new(cx.executor());
33589 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
33590 .await;
33591
33592 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
33593 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
33594 let workspace = window
33595 .read_with(cx, |mw, _| mw.workspace().clone())
33596 .unwrap();
33597 let cx = &mut VisualTestContext::from_window(*window, cx);
33598
33599 let editor = workspace
33600 .update_in(cx, |workspace, window, cx| {
33601 workspace.open_abs_path(
33602 PathBuf::from(path!("/root/file.txt")),
33603 OpenOptions::default(),
33604 window,
33605 cx,
33606 )
33607 })
33608 .await
33609 .unwrap()
33610 .downcast::<Editor>()
33611 .unwrap();
33612
33613 // Enable diff review button mode
33614 editor.update(cx, |editor, cx| {
33615 editor.set_show_diff_review_button(true, cx);
33616 });
33617
33618 // Verify AI is enabled
33619 cx.read(|cx| {
33620 assert!(
33621 !project::DisableAiSettings::get_global(cx).disable_ai,
33622 "AI should be enabled"
33623 );
33624 });
33625
33626 // The show_diff_review_button flag should be true
33627 editor.update(cx, |editor, _cx| {
33628 assert!(
33629 editor.show_diff_review_button(),
33630 "show_diff_review_button should be true"
33631 );
33632 });
33633}
33634
33635/// Helper function to create a DiffHunkKey for testing.
33636/// Uses Anchor::Min as a placeholder anchor since these tests don't need
33637/// real buffer positioning.
33638fn test_hunk_key(file_path: &str) -> DiffHunkKey {
33639 DiffHunkKey {
33640 file_path: if file_path.is_empty() {
33641 Arc::from(util::rel_path::RelPath::empty())
33642 } else {
33643 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
33644 },
33645 hunk_start_anchor: Anchor::Min,
33646 }
33647}
33648
33649/// Helper function to create a DiffHunkKey with a specific anchor for testing.
33650fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
33651 DiffHunkKey {
33652 file_path: if file_path.is_empty() {
33653 Arc::from(util::rel_path::RelPath::empty())
33654 } else {
33655 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
33656 },
33657 hunk_start_anchor: anchor,
33658 }
33659}
33660
33661/// Helper function to add a review comment with default anchors for testing.
33662fn add_test_comment(
33663 editor: &mut Editor,
33664 key: DiffHunkKey,
33665 comment: &str,
33666 cx: &mut Context<Editor>,
33667) -> usize {
33668 editor.add_review_comment(key, comment.to_string(), Anchor::Min..Anchor::Max, cx)
33669}
33670
33671#[gpui::test]
33672fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
33673 init_test(cx, |_| {});
33674
33675 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33676
33677 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33678 let key = test_hunk_key("");
33679
33680 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
33681
33682 let snapshot = editor.buffer().read(cx).snapshot(cx);
33683 assert_eq!(editor.total_review_comment_count(), 1);
33684 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
33685
33686 let comments = editor.comments_for_hunk(&key, &snapshot);
33687 assert_eq!(comments.len(), 1);
33688 assert_eq!(comments[0].comment, "Test comment");
33689 assert_eq!(comments[0].id, id);
33690 });
33691}
33692
33693#[gpui::test]
33694fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
33695 init_test(cx, |_| {});
33696
33697 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33698
33699 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33700 let snapshot = editor.buffer().read(cx).snapshot(cx);
33701 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
33702 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
33703 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
33704 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
33705
33706 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
33707 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
33708
33709 let snapshot = editor.buffer().read(cx).snapshot(cx);
33710 assert_eq!(editor.total_review_comment_count(), 2);
33711 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
33712 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
33713
33714 assert_eq!(
33715 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
33716 "Comment for file1"
33717 );
33718 assert_eq!(
33719 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
33720 "Comment for file2"
33721 );
33722 });
33723}
33724
33725#[gpui::test]
33726fn test_review_comment_remove(cx: &mut TestAppContext) {
33727 init_test(cx, |_| {});
33728
33729 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33730
33731 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33732 let key = test_hunk_key("");
33733
33734 let id = add_test_comment(editor, key, "To be removed", cx);
33735
33736 assert_eq!(editor.total_review_comment_count(), 1);
33737
33738 let removed = editor.remove_review_comment(id, cx);
33739 assert!(removed);
33740 assert_eq!(editor.total_review_comment_count(), 0);
33741
33742 // Try to remove again
33743 let removed_again = editor.remove_review_comment(id, cx);
33744 assert!(!removed_again);
33745 });
33746}
33747
33748#[gpui::test]
33749fn test_review_comment_update(cx: &mut TestAppContext) {
33750 init_test(cx, |_| {});
33751
33752 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33753
33754 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33755 let key = test_hunk_key("");
33756
33757 let id = add_test_comment(editor, key.clone(), "Original text", cx);
33758
33759 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
33760 assert!(updated);
33761
33762 let snapshot = editor.buffer().read(cx).snapshot(cx);
33763 let comments = editor.comments_for_hunk(&key, &snapshot);
33764 assert_eq!(comments[0].comment, "Updated text");
33765 assert!(!comments[0].is_editing); // Should clear editing flag
33766 });
33767}
33768
33769#[gpui::test]
33770fn test_review_comment_take_all(cx: &mut TestAppContext) {
33771 init_test(cx, |_| {});
33772
33773 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33774
33775 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
33776 let snapshot = editor.buffer().read(cx).snapshot(cx);
33777 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
33778 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
33779 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
33780 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
33781
33782 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
33783 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
33784 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
33785
33786 // IDs should be sequential starting from 0
33787 assert_eq!(id1, 0);
33788 assert_eq!(id2, 1);
33789 assert_eq!(id3, 2);
33790
33791 assert_eq!(editor.total_review_comment_count(), 3);
33792
33793 let taken = editor.take_all_review_comments(cx);
33794
33795 // Should have 2 entries (one per hunk)
33796 assert_eq!(taken.len(), 2);
33797
33798 // Total comments should be 3
33799 let total: usize = taken
33800 .iter()
33801 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
33802 .sum();
33803 assert_eq!(total, 3);
33804
33805 // Storage should be empty
33806 assert_eq!(editor.total_review_comment_count(), 0);
33807
33808 // After taking all comments, ID counter should reset
33809 // New comments should get IDs starting from 0 again
33810 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
33811 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
33812
33813 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
33814 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
33815 });
33816}
33817
33818#[gpui::test]
33819fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
33820 init_test(cx, |_| {});
33821
33822 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33823
33824 // Show overlay
33825 editor
33826 .update(cx, |editor, window, cx| {
33827 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33828 })
33829 .unwrap();
33830
33831 // Verify overlay is shown
33832 editor
33833 .update(cx, |editor, _window, cx| {
33834 assert!(!editor.diff_review_overlays.is_empty());
33835 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
33836 assert!(editor.diff_review_prompt_editor().is_some());
33837 })
33838 .unwrap();
33839
33840 // Dismiss overlay
33841 editor
33842 .update(cx, |editor, _window, cx| {
33843 editor.dismiss_all_diff_review_overlays(cx);
33844 })
33845 .unwrap();
33846
33847 // Verify overlay is dismissed
33848 editor
33849 .update(cx, |editor, _window, cx| {
33850 assert!(editor.diff_review_overlays.is_empty());
33851 assert_eq!(editor.diff_review_line_range(cx), None);
33852 assert!(editor.diff_review_prompt_editor().is_none());
33853 })
33854 .unwrap();
33855}
33856
33857#[gpui::test]
33858fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
33859 init_test(cx, |_| {});
33860
33861 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33862
33863 // Show overlay
33864 editor
33865 .update(cx, |editor, window, cx| {
33866 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33867 })
33868 .unwrap();
33869
33870 // Verify overlay is shown
33871 editor
33872 .update(cx, |editor, _window, _cx| {
33873 assert!(!editor.diff_review_overlays.is_empty());
33874 })
33875 .unwrap();
33876
33877 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
33878 editor
33879 .update(cx, |editor, window, cx| {
33880 editor.dismiss_menus_and_popups(true, window, cx);
33881 })
33882 .unwrap();
33883
33884 // Verify overlay is dismissed
33885 editor
33886 .update(cx, |editor, _window, _cx| {
33887 assert!(editor.diff_review_overlays.is_empty());
33888 })
33889 .unwrap();
33890}
33891
33892#[gpui::test]
33893fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
33894 init_test(cx, |_| {});
33895
33896 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33897
33898 // Show overlay
33899 editor
33900 .update(cx, |editor, window, cx| {
33901 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
33902 })
33903 .unwrap();
33904
33905 // Try to submit without typing anything (empty comment)
33906 editor
33907 .update(cx, |editor, window, cx| {
33908 editor.submit_diff_review_comment(window, cx);
33909 })
33910 .unwrap();
33911
33912 // Verify no comment was added
33913 editor
33914 .update(cx, |editor, _window, _cx| {
33915 assert_eq!(editor.total_review_comment_count(), 0);
33916 })
33917 .unwrap();
33918
33919 // Try to submit with whitespace-only comment
33920 editor
33921 .update(cx, |editor, window, cx| {
33922 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
33923 prompt_editor.update(cx, |pe, cx| {
33924 pe.insert(" \n\t ", window, cx);
33925 });
33926 }
33927 editor.submit_diff_review_comment(window, cx);
33928 })
33929 .unwrap();
33930
33931 // Verify still no comment was added
33932 editor
33933 .update(cx, |editor, _window, _cx| {
33934 assert_eq!(editor.total_review_comment_count(), 0);
33935 })
33936 .unwrap();
33937}
33938
33939#[gpui::test]
33940fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
33941 init_test(cx, |_| {});
33942
33943 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33944
33945 // Add a comment directly
33946 let comment_id = editor
33947 .update(cx, |editor, _window, cx| {
33948 let key = test_hunk_key("");
33949 add_test_comment(editor, key, "Original comment", cx)
33950 })
33951 .unwrap();
33952
33953 // Set comment to editing mode
33954 editor
33955 .update(cx, |editor, _window, cx| {
33956 editor.set_comment_editing(comment_id, true, cx);
33957 })
33958 .unwrap();
33959
33960 // Verify editing flag is set
33961 editor
33962 .update(cx, |editor, _window, cx| {
33963 let key = test_hunk_key("");
33964 let snapshot = editor.buffer().read(cx).snapshot(cx);
33965 let comments = editor.comments_for_hunk(&key, &snapshot);
33966 assert_eq!(comments.len(), 1);
33967 assert!(comments[0].is_editing);
33968 })
33969 .unwrap();
33970
33971 // Update the comment
33972 editor
33973 .update(cx, |editor, _window, cx| {
33974 let updated =
33975 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
33976 assert!(updated);
33977 })
33978 .unwrap();
33979
33980 // Verify comment was updated and editing flag is cleared
33981 editor
33982 .update(cx, |editor, _window, cx| {
33983 let key = test_hunk_key("");
33984 let snapshot = editor.buffer().read(cx).snapshot(cx);
33985 let comments = editor.comments_for_hunk(&key, &snapshot);
33986 assert_eq!(comments[0].comment, "Updated comment");
33987 assert!(!comments[0].is_editing);
33988 })
33989 .unwrap();
33990}
33991
33992#[gpui::test]
33993fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
33994 init_test(cx, |_| {});
33995
33996 // Create an editor with some text
33997 let editor = cx.add_window(|window, cx| {
33998 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
33999 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34000 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34001 });
34002
34003 // Add a comment with an anchor on line 2
34004 editor
34005 .update(cx, |editor, _window, cx| {
34006 let snapshot = editor.buffer().read(cx).snapshot(cx);
34007 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
34008 let key = DiffHunkKey {
34009 file_path: Arc::from(util::rel_path::RelPath::empty()),
34010 hunk_start_anchor: anchor,
34011 };
34012 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
34013 assert_eq!(editor.total_review_comment_count(), 1);
34014 })
34015 .unwrap();
34016
34017 // Delete all content (this should orphan the comment's anchor)
34018 editor
34019 .update(cx, |editor, window, cx| {
34020 editor.select_all(&SelectAll, window, cx);
34021 editor.insert("completely new content", window, cx);
34022 })
34023 .unwrap();
34024
34025 // Trigger cleanup
34026 editor
34027 .update(cx, |editor, _window, cx| {
34028 editor.cleanup_orphaned_review_comments(cx);
34029 // Comment should be removed because its anchor is invalid
34030 assert_eq!(editor.total_review_comment_count(), 0);
34031 })
34032 .unwrap();
34033}
34034
34035#[gpui::test]
34036fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
34037 init_test(cx, |_| {});
34038
34039 // Create an editor with some text
34040 let editor = cx.add_window(|window, cx| {
34041 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
34042 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34043 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34044 });
34045
34046 // Add a comment with an anchor on line 2
34047 editor
34048 .update(cx, |editor, _window, cx| {
34049 let snapshot = editor.buffer().read(cx).snapshot(cx);
34050 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
34051 let key = DiffHunkKey {
34052 file_path: Arc::from(util::rel_path::RelPath::empty()),
34053 hunk_start_anchor: anchor,
34054 };
34055 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
34056 assert_eq!(editor.total_review_comment_count(), 1);
34057 })
34058 .unwrap();
34059
34060 // Edit the buffer - this should trigger cleanup via on_buffer_event
34061 // Delete all content which orphans the anchor
34062 editor
34063 .update(cx, |editor, window, cx| {
34064 editor.select_all(&SelectAll, window, cx);
34065 editor.insert("completely new content", window, cx);
34066 // The cleanup is called automatically in on_buffer_event when Edited fires
34067 })
34068 .unwrap();
34069
34070 // Verify cleanup happened automatically (not manually triggered)
34071 editor
34072 .update(cx, |editor, _window, _cx| {
34073 // Comment should be removed because its anchor became invalid
34074 // and cleanup was called automatically on buffer edit
34075 assert_eq!(editor.total_review_comment_count(), 0);
34076 })
34077 .unwrap();
34078}
34079
34080#[gpui::test]
34081fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
34082 init_test(cx, |_| {});
34083
34084 // This test verifies that comments can be stored for multiple different hunks
34085 // and that hunk_comment_count correctly identifies comments per hunk.
34086 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34087
34088 _ = editor.update(cx, |editor, _window, cx| {
34089 let snapshot = editor.buffer().read(cx).snapshot(cx);
34090
34091 // Create two different hunk keys (simulating two different files)
34092 let anchor = snapshot.anchor_before(Point::new(0, 0));
34093 let key1 = DiffHunkKey {
34094 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
34095 hunk_start_anchor: anchor,
34096 };
34097 let key2 = DiffHunkKey {
34098 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
34099 hunk_start_anchor: anchor,
34100 };
34101
34102 // Add comments to first hunk
34103 editor.add_review_comment(
34104 key1.clone(),
34105 "Comment 1 for file1".to_string(),
34106 anchor..anchor,
34107 cx,
34108 );
34109 editor.add_review_comment(
34110 key1.clone(),
34111 "Comment 2 for file1".to_string(),
34112 anchor..anchor,
34113 cx,
34114 );
34115
34116 // Add comment to second hunk
34117 editor.add_review_comment(
34118 key2.clone(),
34119 "Comment for file2".to_string(),
34120 anchor..anchor,
34121 cx,
34122 );
34123
34124 // Verify total count
34125 assert_eq!(editor.total_review_comment_count(), 3);
34126
34127 // Verify per-hunk counts
34128 let snapshot = editor.buffer().read(cx).snapshot(cx);
34129 assert_eq!(
34130 editor.hunk_comment_count(&key1, &snapshot),
34131 2,
34132 "file1 should have 2 comments"
34133 );
34134 assert_eq!(
34135 editor.hunk_comment_count(&key2, &snapshot),
34136 1,
34137 "file2 should have 1 comment"
34138 );
34139
34140 // Verify comments_for_hunk returns correct comments
34141 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
34142 assert_eq!(file1_comments.len(), 2);
34143 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
34144 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
34145
34146 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
34147 assert_eq!(file2_comments.len(), 1);
34148 assert_eq!(file2_comments[0].comment, "Comment for file2");
34149 });
34150}
34151
34152#[gpui::test]
34153fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
34154 init_test(cx, |_| {});
34155
34156 // This test verifies that hunk_keys_match correctly identifies when two
34157 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
34158 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34159
34160 _ = editor.update(cx, |editor, _window, cx| {
34161 let snapshot = editor.buffer().read(cx).snapshot(cx);
34162 let anchor = snapshot.anchor_before(Point::new(0, 0));
34163
34164 // Create two keys with the same file path and anchor
34165 let key1 = DiffHunkKey {
34166 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
34167 hunk_start_anchor: anchor,
34168 };
34169 let key2 = DiffHunkKey {
34170 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
34171 hunk_start_anchor: anchor,
34172 };
34173
34174 // Add comment to first key
34175 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
34176
34177 // Verify second key (same hunk) finds the comment
34178 let snapshot = editor.buffer().read(cx).snapshot(cx);
34179 assert_eq!(
34180 editor.hunk_comment_count(&key2, &snapshot),
34181 1,
34182 "Same hunk should find the comment"
34183 );
34184
34185 // Create a key with different file path
34186 let different_file_key = DiffHunkKey {
34187 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
34188 hunk_start_anchor: anchor,
34189 };
34190
34191 // Different file should not find the comment
34192 assert_eq!(
34193 editor.hunk_comment_count(&different_file_key, &snapshot),
34194 0,
34195 "Different file should not find the comment"
34196 );
34197 });
34198}
34199
34200#[gpui::test]
34201fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
34202 init_test(cx, |_| {});
34203
34204 // This test verifies that set_diff_review_comments_expanded correctly
34205 // updates the expanded state of overlays.
34206 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34207
34208 // Show overlay
34209 editor
34210 .update(cx, |editor, window, cx| {
34211 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
34212 })
34213 .unwrap();
34214
34215 // Verify initially expanded (default)
34216 editor
34217 .update(cx, |editor, _window, _cx| {
34218 assert!(
34219 editor.diff_review_overlays[0].comments_expanded,
34220 "Should be expanded by default"
34221 );
34222 })
34223 .unwrap();
34224
34225 // Set to collapsed using the public method
34226 editor
34227 .update(cx, |editor, _window, cx| {
34228 editor.set_diff_review_comments_expanded(false, cx);
34229 })
34230 .unwrap();
34231
34232 // Verify collapsed
34233 editor
34234 .update(cx, |editor, _window, _cx| {
34235 assert!(
34236 !editor.diff_review_overlays[0].comments_expanded,
34237 "Should be collapsed after setting to false"
34238 );
34239 })
34240 .unwrap();
34241
34242 // Set back to expanded
34243 editor
34244 .update(cx, |editor, _window, cx| {
34245 editor.set_diff_review_comments_expanded(true, cx);
34246 })
34247 .unwrap();
34248
34249 // Verify expanded again
34250 editor
34251 .update(cx, |editor, _window, _cx| {
34252 assert!(
34253 editor.diff_review_overlays[0].comments_expanded,
34254 "Should be expanded after setting to true"
34255 );
34256 })
34257 .unwrap();
34258}
34259
34260#[gpui::test]
34261fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
34262 init_test(cx, |_| {});
34263
34264 // Create an editor with multiple lines of text
34265 let editor = cx.add_window(|window, cx| {
34266 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
34267 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34268 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34269 });
34270
34271 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
34272 editor
34273 .update(cx, |editor, window, cx| {
34274 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
34275 })
34276 .unwrap();
34277
34278 // Verify line range
34279 editor
34280 .update(cx, |editor, _window, cx| {
34281 assert!(!editor.diff_review_overlays.is_empty());
34282 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
34283 })
34284 .unwrap();
34285
34286 // Dismiss and test with reversed range (end < start)
34287 editor
34288 .update(cx, |editor, _window, cx| {
34289 editor.dismiss_all_diff_review_overlays(cx);
34290 })
34291 .unwrap();
34292
34293 // Show overlay with reversed range - should normalize it
34294 editor
34295 .update(cx, |editor, window, cx| {
34296 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
34297 })
34298 .unwrap();
34299
34300 // Verify range is normalized (start <= end)
34301 editor
34302 .update(cx, |editor, _window, cx| {
34303 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
34304 })
34305 .unwrap();
34306}
34307
34308#[gpui::test]
34309fn test_diff_review_drag_state(cx: &mut TestAppContext) {
34310 init_test(cx, |_| {});
34311
34312 let editor = cx.add_window(|window, cx| {
34313 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
34314 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34315 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
34316 });
34317
34318 // Initially no drag state
34319 editor
34320 .update(cx, |editor, _window, _cx| {
34321 assert!(editor.diff_review_drag_state.is_none());
34322 })
34323 .unwrap();
34324
34325 // Start drag at row 1
34326 editor
34327 .update(cx, |editor, window, cx| {
34328 editor.start_diff_review_drag(DisplayRow(1), window, cx);
34329 })
34330 .unwrap();
34331
34332 // Verify drag state is set
34333 editor
34334 .update(cx, |editor, window, cx| {
34335 assert!(editor.diff_review_drag_state.is_some());
34336 let snapshot = editor.snapshot(window, cx);
34337 let range = editor
34338 .diff_review_drag_state
34339 .as_ref()
34340 .unwrap()
34341 .row_range(&snapshot.display_snapshot);
34342 assert_eq!(*range.start(), DisplayRow(1));
34343 assert_eq!(*range.end(), DisplayRow(1));
34344 })
34345 .unwrap();
34346
34347 // Update drag to row 3
34348 editor
34349 .update(cx, |editor, window, cx| {
34350 editor.update_diff_review_drag(DisplayRow(3), window, cx);
34351 })
34352 .unwrap();
34353
34354 // Verify drag state is updated
34355 editor
34356 .update(cx, |editor, window, cx| {
34357 assert!(editor.diff_review_drag_state.is_some());
34358 let snapshot = editor.snapshot(window, cx);
34359 let range = editor
34360 .diff_review_drag_state
34361 .as_ref()
34362 .unwrap()
34363 .row_range(&snapshot.display_snapshot);
34364 assert_eq!(*range.start(), DisplayRow(1));
34365 assert_eq!(*range.end(), DisplayRow(3));
34366 })
34367 .unwrap();
34368
34369 // End drag - should show overlay
34370 editor
34371 .update(cx, |editor, window, cx| {
34372 editor.end_diff_review_drag(window, cx);
34373 })
34374 .unwrap();
34375
34376 // Verify drag state is cleared and overlay is shown
34377 editor
34378 .update(cx, |editor, _window, cx| {
34379 assert!(editor.diff_review_drag_state.is_none());
34380 assert!(!editor.diff_review_overlays.is_empty());
34381 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
34382 })
34383 .unwrap();
34384}
34385
34386#[gpui::test]
34387fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
34388 init_test(cx, |_| {});
34389
34390 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34391
34392 // Start drag
34393 editor
34394 .update(cx, |editor, window, cx| {
34395 editor.start_diff_review_drag(DisplayRow(0), window, cx);
34396 })
34397 .unwrap();
34398
34399 // Verify drag state is set
34400 editor
34401 .update(cx, |editor, _window, _cx| {
34402 assert!(editor.diff_review_drag_state.is_some());
34403 })
34404 .unwrap();
34405
34406 // Cancel drag
34407 editor
34408 .update(cx, |editor, _window, cx| {
34409 editor.cancel_diff_review_drag(cx);
34410 })
34411 .unwrap();
34412
34413 // Verify drag state is cleared and no overlay was created
34414 editor
34415 .update(cx, |editor, _window, _cx| {
34416 assert!(editor.diff_review_drag_state.is_none());
34417 assert!(editor.diff_review_overlays.is_empty());
34418 })
34419 .unwrap();
34420}
34421
34422#[gpui::test]
34423fn test_calculate_overlay_height(cx: &mut TestAppContext) {
34424 init_test(cx, |_| {});
34425
34426 // This test verifies that calculate_overlay_height returns correct heights
34427 // based on comment count and expanded state.
34428 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
34429
34430 _ = editor.update(cx, |editor, _window, cx| {
34431 let snapshot = editor.buffer().read(cx).snapshot(cx);
34432 let anchor = snapshot.anchor_before(Point::new(0, 0));
34433 let key = DiffHunkKey {
34434 file_path: Arc::from(util::rel_path::RelPath::empty()),
34435 hunk_start_anchor: anchor,
34436 };
34437
34438 // No comments: base height of 2
34439 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
34440 assert_eq!(
34441 height_no_comments, 2,
34442 "Base height should be 2 with no comments"
34443 );
34444
34445 // Add one comment
34446 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
34447
34448 let snapshot = editor.buffer().read(cx).snapshot(cx);
34449
34450 // With comments expanded: base (2) + header (1) + 2 per comment
34451 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
34452 assert_eq!(
34453 height_expanded,
34454 2 + 1 + 2, // base + header + 1 comment * 2
34455 "Height with 1 comment expanded"
34456 );
34457
34458 // With comments collapsed: base (2) + header (1)
34459 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
34460 assert_eq!(
34461 height_collapsed,
34462 2 + 1, // base + header only
34463 "Height with comments collapsed"
34464 );
34465
34466 // Add more comments
34467 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
34468 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
34469
34470 let snapshot = editor.buffer().read(cx).snapshot(cx);
34471
34472 // With 3 comments expanded
34473 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
34474 assert_eq!(
34475 height_3_expanded,
34476 2 + 1 + (3 * 2), // base + header + 3 comments * 2
34477 "Height with 3 comments expanded"
34478 );
34479
34480 // Collapsed height stays the same regardless of comment count
34481 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
34482 assert_eq!(
34483 height_3_collapsed,
34484 2 + 1, // base + header only
34485 "Height with 3 comments collapsed should be same as 1 comment collapsed"
34486 );
34487 });
34488}
34489
34490#[gpui::test]
34491async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
34492 init_test(cx, |_| {});
34493
34494 let language = Arc::new(Language::new(
34495 LanguageConfig::default(),
34496 Some(tree_sitter_rust::LANGUAGE.into()),
34497 ));
34498
34499 let text = r#"
34500 fn main() {
34501 let x = foo(1, 2);
34502 }
34503 "#
34504 .unindent();
34505
34506 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34507 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34508 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34509
34510 editor
34511 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34512 .await;
34513
34514 // Test case 1: Move to end of syntax nodes
34515 editor.update_in(cx, |editor, window, cx| {
34516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34517 s.select_display_ranges([
34518 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
34519 ]);
34520 });
34521 });
34522 editor.update(cx, |editor, cx| {
34523 assert_text_with_selections(
34524 editor,
34525 indoc! {r#"
34526 fn main() {
34527 let x = foo(ˇ1, 2);
34528 }
34529 "#},
34530 cx,
34531 );
34532 });
34533 editor.update_in(cx, |editor, window, cx| {
34534 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34535 });
34536 editor.update(cx, |editor, cx| {
34537 assert_text_with_selections(
34538 editor,
34539 indoc! {r#"
34540 fn main() {
34541 let x = foo(1ˇ, 2);
34542 }
34543 "#},
34544 cx,
34545 );
34546 });
34547 editor.update_in(cx, |editor, window, cx| {
34548 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
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
34590 // Test case 2: Move to start of syntax nodes
34591 editor.update_in(cx, |editor, window, cx| {
34592 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34593 s.select_display_ranges([
34594 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
34595 ]);
34596 });
34597 });
34598 editor.update(cx, |editor, cx| {
34599 assert_text_with_selections(
34600 editor,
34601 indoc! {r#"
34602 fn main() {
34603 let x = foo(1, 2ˇ);
34604 }
34605 "#},
34606 cx,
34607 );
34608 });
34609 editor.update_in(cx, |editor, window, cx| {
34610 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34611 });
34612 editor.update(cx, |editor, cx| {
34613 assert_text_with_selections(
34614 editor,
34615 indoc! {r#"
34616 fn main() {
34617 let x = fooˇ(1, 2);
34618 }
34619 "#},
34620 cx,
34621 );
34622 });
34623 editor.update_in(cx, |editor, window, cx| {
34624 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
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}
34680
34681#[gpui::test]
34682async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
34683 init_test(cx, |_| {});
34684
34685 let language = Arc::new(Language::new(
34686 LanguageConfig::default(),
34687 Some(tree_sitter_rust::LANGUAGE.into()),
34688 ));
34689
34690 let text = r#"
34691 fn main() {
34692 let x = foo(1, 2);
34693 let y = bar(3, 4);
34694 }
34695 "#
34696 .unindent();
34697
34698 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34699 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34700 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34701
34702 editor
34703 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34704 .await;
34705
34706 // Test case 1: Move to end of syntax nodes with two cursors
34707 editor.update_in(cx, |editor, window, cx| {
34708 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34709 s.select_display_ranges([
34710 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
34711 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
34712 ]);
34713 });
34714 });
34715 editor.update(cx, |editor, cx| {
34716 assert_text_with_selections(
34717 editor,
34718 indoc! {r#"
34719 fn main() {
34720 let x = foo(1, 2ˇ);
34721 let y = bar(3, 4ˇ);
34722 }
34723 "#},
34724 cx,
34725 );
34726 });
34727 editor.update_in(cx, |editor, window, cx| {
34728 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34729 });
34730 editor.update(cx, |editor, cx| {
34731 assert_text_with_selections(
34732 editor,
34733 indoc! {r#"
34734 fn main() {
34735 let x = foo(1, 2)ˇ;
34736 let y = bar(3, 4)ˇ;
34737 }
34738 "#},
34739 cx,
34740 );
34741 });
34742 editor.update_in(cx, |editor, window, cx| {
34743 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34744 });
34745 editor.update(cx, |editor, cx| {
34746 assert_text_with_selections(
34747 editor,
34748 indoc! {r#"
34749 fn main() {
34750 let x = foo(1, 2);ˇ
34751 let y = bar(3, 4);ˇ
34752 }
34753 "#},
34754 cx,
34755 );
34756 });
34757
34758 // Test case 2: Move to start of syntax nodes with two cursors
34759 editor.update_in(cx, |editor, window, cx| {
34760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34761 s.select_display_ranges([
34762 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
34763 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
34764 ]);
34765 });
34766 });
34767 editor.update(cx, |editor, cx| {
34768 assert_text_with_selections(
34769 editor,
34770 indoc! {r#"
34771 fn main() {
34772 let x = foo(1, ˇ2);
34773 let y = bar(3, ˇ4);
34774 }
34775 "#},
34776 cx,
34777 );
34778 });
34779 editor.update_in(cx, |editor, window, cx| {
34780 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34781 });
34782 editor.update(cx, |editor, cx| {
34783 assert_text_with_selections(
34784 editor,
34785 indoc! {r#"
34786 fn main() {
34787 let x = fooˇ(1, 2);
34788 let y = barˇ(3, 4);
34789 }
34790 "#},
34791 cx,
34792 );
34793 });
34794 editor.update_in(cx, |editor, window, cx| {
34795 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34796 });
34797 editor.update(cx, |editor, cx| {
34798 assert_text_with_selections(
34799 editor,
34800 indoc! {r#"
34801 fn main() {
34802 let x = ˇfoo(1, 2);
34803 let y = ˇbar(3, 4);
34804 }
34805 "#},
34806 cx,
34807 );
34808 });
34809 editor.update_in(cx, |editor, window, cx| {
34810 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34811 });
34812 editor.update(cx, |editor, cx| {
34813 assert_text_with_selections(
34814 editor,
34815 indoc! {r#"
34816 fn main() {
34817 ˇlet x = foo(1, 2);
34818 ˇlet y = bar(3, 4);
34819 }
34820 "#},
34821 cx,
34822 );
34823 });
34824}
34825
34826#[gpui::test]
34827async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
34828 cx: &mut TestAppContext,
34829) {
34830 init_test(cx, |_| {});
34831
34832 let language = Arc::new(Language::new(
34833 LanguageConfig::default(),
34834 Some(tree_sitter_rust::LANGUAGE.into()),
34835 ));
34836
34837 let text = r#"
34838 fn main() {
34839 let x = foo(1, 2);
34840 let msg = "hello world";
34841 }
34842 "#
34843 .unindent();
34844
34845 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
34846 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34847 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34848
34849 editor
34850 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34851 .await;
34852
34853 // Test case 1: With existing selection, move_to_end keeps selection
34854 editor.update_in(cx, |editor, window, cx| {
34855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34856 s.select_display_ranges([
34857 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
34858 ]);
34859 });
34860 });
34861 editor.update(cx, |editor, cx| {
34862 assert_text_with_selections(
34863 editor,
34864 indoc! {r#"
34865 fn main() {
34866 let x = «foo(1, 2)ˇ»;
34867 let msg = "hello world";
34868 }
34869 "#},
34870 cx,
34871 );
34872 });
34873 editor.update_in(cx, |editor, window, cx| {
34874 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34875 });
34876 editor.update(cx, |editor, cx| {
34877 assert_text_with_selections(
34878 editor,
34879 indoc! {r#"
34880 fn main() {
34881 let x = «foo(1, 2)ˇ»;
34882 let msg = "hello world";
34883 }
34884 "#},
34885 cx,
34886 );
34887 });
34888
34889 // Test case 2: Move to end within a string
34890 editor.update_in(cx, |editor, window, cx| {
34891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34892 s.select_display_ranges([
34893 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
34894 ]);
34895 });
34896 });
34897 editor.update(cx, |editor, cx| {
34898 assert_text_with_selections(
34899 editor,
34900 indoc! {r#"
34901 fn main() {
34902 let x = foo(1, 2);
34903 let msg = "ˇhello world";
34904 }
34905 "#},
34906 cx,
34907 );
34908 });
34909 editor.update_in(cx, |editor, window, cx| {
34910 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
34911 });
34912 editor.update(cx, |editor, cx| {
34913 assert_text_with_selections(
34914 editor,
34915 indoc! {r#"
34916 fn main() {
34917 let x = foo(1, 2);
34918 let msg = "hello worldˇ";
34919 }
34920 "#},
34921 cx,
34922 );
34923 });
34924
34925 // Test case 3: Move to start within a string
34926 editor.update_in(cx, |editor, window, cx| {
34927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34928 s.select_display_ranges([
34929 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
34930 ]);
34931 });
34932 });
34933 editor.update(cx, |editor, cx| {
34934 assert_text_with_selections(
34935 editor,
34936 indoc! {r#"
34937 fn main() {
34938 let x = foo(1, 2);
34939 let msg = "hello ˇworld";
34940 }
34941 "#},
34942 cx,
34943 );
34944 });
34945 editor.update_in(cx, |editor, window, cx| {
34946 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
34947 });
34948 editor.update(cx, |editor, cx| {
34949 assert_text_with_selections(
34950 editor,
34951 indoc! {r#"
34952 fn main() {
34953 let x = foo(1, 2);
34954 let msg = "ˇhello world";
34955 }
34956 "#},
34957 cx,
34958 );
34959 });
34960}
34961
34962#[gpui::test]
34963async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
34964 init_test(cx, |_| {});
34965
34966 let language = Arc::new(Language::new(
34967 LanguageConfig::default(),
34968 Some(tree_sitter_rust::LANGUAGE.into()),
34969 ));
34970
34971 // Test Group 1.1: Cursor in String - First Jump (Select to End)
34972 let text = r#"let msg = "foo bar baz";"#.unindent();
34973
34974 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
34975 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34976 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34977
34978 editor
34979 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34980 .await;
34981
34982 editor.update_in(cx, |editor, window, cx| {
34983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34984 s.select_display_ranges([
34985 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
34986 ]);
34987 });
34988 });
34989 editor.update(cx, |editor, cx| {
34990 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
34991 });
34992 editor.update_in(cx, |editor, window, cx| {
34993 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
34994 });
34995 editor.update(cx, |editor, cx| {
34996 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
34997 });
34998
34999 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
35000 editor.update_in(cx, |editor, window, cx| {
35001 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35002 });
35003 editor.update(cx, |editor, cx| {
35004 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
35005 });
35006
35007 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
35008 editor.update_in(cx, |editor, window, cx| {
35009 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35010 });
35011 editor.update(cx, |editor, cx| {
35012 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
35013 });
35014
35015 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
35016 editor.update_in(cx, |editor, window, cx| {
35017 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35018 s.select_display_ranges([
35019 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
35020 ]);
35021 });
35022 });
35023 editor.update(cx, |editor, cx| {
35024 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
35025 });
35026 editor.update_in(cx, |editor, window, cx| {
35027 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35028 });
35029 editor.update(cx, |editor, cx| {
35030 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
35031 });
35032
35033 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
35034 editor.update_in(cx, |editor, window, cx| {
35035 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35036 });
35037 editor.update(cx, |editor, cx| {
35038 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
35039 });
35040
35041 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
35042 editor.update_in(cx, |editor, window, cx| {
35043 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35044 });
35045 editor.update(cx, |editor, cx| {
35046 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
35047 });
35048
35049 // Test Group 2.1: Let Statement Progression (Select to End)
35050 let text = r#"
35051fn main() {
35052 let x = "hello";
35053}
35054"#
35055 .unindent();
35056
35057 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35058 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35059 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35060
35061 editor
35062 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35063 .await;
35064
35065 editor.update_in(cx, |editor, window, cx| {
35066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35067 s.select_display_ranges([
35068 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
35069 ]);
35070 });
35071 });
35072 editor.update(cx, |editor, cx| {
35073 assert_text_with_selections(
35074 editor,
35075 indoc! {r#"
35076 fn main() {
35077 let xˇ = "hello";
35078 }
35079 "#},
35080 cx,
35081 );
35082 });
35083 editor.update_in(cx, |editor, window, cx| {
35084 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35085 });
35086 editor.update(cx, |editor, cx| {
35087 assert_text_with_selections(
35088 editor,
35089 indoc! {r##"
35090 fn main() {
35091 let x« = "hello";ˇ»
35092 }
35093 "##},
35094 cx,
35095 );
35096 });
35097 editor.update_in(cx, |editor, window, cx| {
35098 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
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
35112 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
35113 let text = r#"let x = "hello";"#.unindent();
35114
35115 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35116 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35117 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35118
35119 editor
35120 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35121 .await;
35122
35123 editor.update_in(cx, |editor, window, cx| {
35124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35125 s.select_display_ranges([
35126 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
35127 ]);
35128 });
35129 });
35130 editor.update(cx, |editor, cx| {
35131 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
35132 });
35133 editor.update_in(cx, |editor, window, cx| {
35134 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35135 });
35136 editor.update(cx, |editor, cx| {
35137 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
35138 });
35139
35140 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
35141 editor.update_in(cx, |editor, window, cx| {
35142 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35143 s.select_display_ranges([
35144 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
35145 ]);
35146 });
35147 });
35148 editor.update(cx, |editor, cx| {
35149 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
35150 });
35151 editor.update_in(cx, |editor, window, cx| {
35152 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
35153 });
35154 editor.update(cx, |editor, cx| {
35155 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
35156 });
35157
35158 // Test Group 3.1: Create Selection from Cursor (Select to End)
35159 let text = r#"let x = "hello world";"#.unindent();
35160
35161 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35162 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35163 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35164
35165 editor
35166 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35167 .await;
35168
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), 14)..DisplayPoint::new(DisplayRow(0), 14)
35173 ]);
35174 });
35175 });
35176 editor.update(cx, |editor, cx| {
35177 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
35178 });
35179 editor.update_in(cx, |editor, window, cx| {
35180 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35181 });
35182 editor.update(cx, |editor, cx| {
35183 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
35184 });
35185
35186 // Test Group 3.2: Extend Existing Selection (Select to End)
35187 editor.update_in(cx, |editor, window, cx| {
35188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35189 s.select_display_ranges([
35190 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
35191 ]);
35192 });
35193 });
35194 editor.update(cx, |editor, cx| {
35195 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
35196 });
35197 editor.update_in(cx, |editor, window, cx| {
35198 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35199 });
35200 editor.update(cx, |editor, cx| {
35201 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
35202 });
35203
35204 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
35205 let text = r#"let x = "hello"; let y = 42;"#.unindent();
35206
35207 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35208 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35209 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35210
35211 editor
35212 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35213 .await;
35214
35215 editor.update_in(cx, |editor, window, cx| {
35216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35217 s.select_display_ranges([
35218 // Cursor inside string content
35219 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
35220 // Cursor at let statement semicolon
35221 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
35222 // Cursor inside integer literal
35223 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
35224 ]);
35225 });
35226 });
35227 editor.update(cx, |editor, cx| {
35228 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
35229 });
35230 editor.update_in(cx, |editor, window, cx| {
35231 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35232 });
35233 editor.update(cx, |editor, cx| {
35234 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
35235 });
35236
35237 // Test Group 4.2: Multiple Cursors on Separate Lines
35238 let text = r#"
35239let x = "hello";
35240let y = 42;
35241"#
35242 .unindent();
35243
35244 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35245 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35246 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35247
35248 editor
35249 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35250 .await;
35251
35252 editor.update_in(cx, |editor, window, cx| {
35253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35254 s.select_display_ranges([
35255 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
35256 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
35257 ]);
35258 });
35259 });
35260
35261 editor.update(cx, |editor, cx| {
35262 assert_text_with_selections(
35263 editor,
35264 indoc! {r#"
35265 let x = "helˇlo";
35266 let y = 4ˇ2;
35267 "#},
35268 cx,
35269 );
35270 });
35271 editor.update_in(cx, |editor, window, cx| {
35272 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35273 });
35274 editor.update(cx, |editor, cx| {
35275 assert_text_with_selections(
35276 editor,
35277 indoc! {r#"
35278 let x = "hel«loˇ»";
35279 let y = 4«2ˇ»;
35280 "#},
35281 cx,
35282 );
35283 });
35284
35285 // Test Group 5.1: Nested Function Calls
35286 let text = r#"let result = foo(bar("arg"));"#.unindent();
35287
35288 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35289 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35290 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35291
35292 editor
35293 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35294 .await;
35295
35296 editor.update_in(cx, |editor, window, cx| {
35297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35298 s.select_display_ranges([
35299 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
35300 ]);
35301 });
35302 });
35303 editor.update(cx, |editor, cx| {
35304 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
35305 });
35306 editor.update_in(cx, |editor, window, cx| {
35307 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35308 });
35309 editor.update(cx, |editor, cx| {
35310 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
35311 });
35312 editor.update_in(cx, |editor, window, cx| {
35313 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35314 });
35315 editor.update(cx, |editor, cx| {
35316 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
35317 });
35318 editor.update_in(cx, |editor, window, cx| {
35319 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35320 });
35321 editor.update(cx, |editor, cx| {
35322 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
35323 });
35324
35325 // Test Group 6.1: Block Comments
35326 let text = r#"let x = /* multi
35327 line
35328 comment */;"#
35329 .unindent();
35330
35331 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35332 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35333 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35334
35335 editor
35336 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35337 .await;
35338
35339 editor.update_in(cx, |editor, window, cx| {
35340 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35341 s.select_display_ranges([
35342 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
35343 ]);
35344 });
35345 });
35346 editor.update(cx, |editor, cx| {
35347 assert_text_with_selections(
35348 editor,
35349 indoc! {r#"
35350let x = /* multiˇ
35351line
35352comment */;"#},
35353 cx,
35354 );
35355 });
35356 editor.update_in(cx, |editor, window, cx| {
35357 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35358 });
35359 editor.update(cx, |editor, cx| {
35360 assert_text_with_selections(
35361 editor,
35362 indoc! {r#"
35363let x = /* multi«
35364line
35365comment */ˇ»;"#},
35366 cx,
35367 );
35368 });
35369
35370 // Test Group 6.2: Array/Vector Literals
35371 let text = r#"let arr = [1, 2, 3];"#.unindent();
35372
35373 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
35374 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
35375 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
35376
35377 editor
35378 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
35379 .await;
35380
35381 editor.update_in(cx, |editor, window, cx| {
35382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
35383 s.select_display_ranges([
35384 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
35385 ]);
35386 });
35387 });
35388 editor.update(cx, |editor, cx| {
35389 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
35390 });
35391 editor.update_in(cx, |editor, window, cx| {
35392 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35393 });
35394 editor.update(cx, |editor, cx| {
35395 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
35396 });
35397 editor.update_in(cx, |editor, window, cx| {
35398 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
35399 });
35400 editor.update(cx, |editor, cx| {
35401 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
35402 });
35403}
35404
35405#[gpui::test]
35406async fn test_restore_and_next(cx: &mut TestAppContext) {
35407 init_test(cx, |_| {});
35408 let mut cx = EditorTestContext::new(cx).await;
35409
35410 let diff_base = r#"
35411 one
35412 two
35413 three
35414 four
35415 five
35416 "#
35417 .unindent();
35418
35419 cx.set_state(
35420 &r#"
35421 ONE
35422 two
35423 ˇTHREE
35424 four
35425 FIVE
35426 "#
35427 .unindent(),
35428 );
35429 cx.set_head_text(&diff_base);
35430
35431 cx.update_editor(|editor, window, cx| {
35432 editor.set_expand_all_diff_hunks(cx);
35433 editor.restore_and_next(&Default::default(), window, cx);
35434 });
35435 cx.run_until_parked();
35436
35437 cx.assert_state_with_diff(
35438 r#"
35439 - one
35440 + ONE
35441 two
35442 three
35443 four
35444 - ˇfive
35445 + FIVE
35446 "#
35447 .unindent(),
35448 );
35449
35450 cx.update_editor(|editor, window, cx| {
35451 editor.restore_and_next(&Default::default(), window, cx);
35452 });
35453 cx.run_until_parked();
35454
35455 cx.assert_state_with_diff(
35456 r#"
35457 - one
35458 + ONE
35459 two
35460 three
35461 four
35462 ˇfive
35463 "#
35464 .unindent(),
35465 );
35466}
35467
35468#[gpui::test]
35469async fn test_restore_hunk_with_stale_base_text(cx: &mut TestAppContext) {
35470 // Regression test: prepare_restore_change must read base_text from the same
35471 // snapshot the hunk came from, not from the live BufferDiff entity. The live
35472 // entity's base_text may have already been updated asynchronously (e.g.
35473 // because git HEAD changed) while the MultiBufferSnapshot still holds the
35474 // old hunk byte ranges — using both together causes Rope::slice to panic
35475 // when the old range exceeds the new base text length.
35476 init_test(cx, |_| {});
35477 let mut cx = EditorTestContext::new(cx).await;
35478
35479 let long_base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n";
35480 cx.set_state("ˇONE\ntwo\nTHREE\nfour\nFIVE\nsix\nseven\neight\nnine\nten\n");
35481 cx.set_head_text(long_base_text);
35482
35483 let buffer_id = cx.update_buffer(|buffer, _| buffer.remote_id());
35484
35485 // Verify we have hunks from the initial diff.
35486 let has_hunks = cx.update_editor(|editor, window, cx| {
35487 let snapshot = editor.snapshot(window, cx);
35488 let hunks = snapshot
35489 .buffer_snapshot()
35490 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len());
35491 hunks.count() > 0
35492 });
35493 assert!(has_hunks, "should have diff hunks before restoring");
35494
35495 // Now trigger a git HEAD change to a much shorter base text.
35496 // After this, the live BufferDiff entity's base_text buffer will be
35497 // updated synchronously (inside set_snapshot_with_secondary_inner),
35498 // but DiffChanged is deferred until parsing_idle completes.
35499 // We step the executor tick-by-tick to find the window where the
35500 // live base_text is already short but the MultiBuffer snapshot is
35501 // still stale (old hunks + old base_text).
35502 let short_base_text = "short\n";
35503 let fs = cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
35504 let path = cx.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
35505 fs.set_head_for_repo(
35506 &Path::new(path!("/root")).join(".git"),
35507 &[(path.as_unix_str(), short_base_text.to_string())],
35508 "newcommit",
35509 );
35510
35511 // Step the executor tick-by-tick. At each step, check whether the
35512 // race condition exists: live BufferDiff has short base text but
35513 // the MultiBuffer snapshot still has old (long) hunks.
35514 let mut found_race = false;
35515 for _ in 0..200 {
35516 cx.executor().tick();
35517
35518 let race_exists = cx.update_editor(|editor, _window, cx| {
35519 let multi_buffer = editor.buffer().read(cx);
35520 let diff_entity = match multi_buffer.diff_for(buffer_id) {
35521 Some(d) => d,
35522 None => return false,
35523 };
35524 let live_base_len = diff_entity.read(cx).base_text(cx).len();
35525 let snapshot = multi_buffer.snapshot(cx);
35526 let snapshot_base_len = snapshot
35527 .diff_for_buffer_id(buffer_id)
35528 .map(|d| d.base_text().len());
35529 // Race: live base text is shorter than what the snapshot knows.
35530 live_base_len < long_base_text.len() && snapshot_base_len == Some(long_base_text.len())
35531 });
35532
35533 if race_exists {
35534 found_race = true;
35535 // The race window is open: the live entity has new (short) base
35536 // text but the MultiBuffer snapshot still has old hunks with byte
35537 // ranges computed against the old long base text. Attempt restore.
35538 // Without the fix, this panics with "cannot summarize past end of
35539 // rope". With the fix, it reads base_text from the stale snapshot
35540 // (consistent with the stale hunks) and succeeds.
35541 cx.update_editor(|editor, window, cx| {
35542 editor.select_all(&SelectAll, window, cx);
35543 editor.git_restore(&Default::default(), window, cx);
35544 });
35545 break;
35546 }
35547 }
35548
35549 assert!(
35550 found_race,
35551 "failed to observe the race condition between \
35552 live BufferDiff base_text and stale MultiBuffer snapshot; \
35553 the test may need adjustment if the async diff pipeline changed"
35554 );
35555}
35556
35557#[gpui::test]
35558async fn test_align_selections(cx: &mut TestAppContext) {
35559 init_test(cx, |_| {});
35560 let mut cx = EditorTestContext::new(cx).await;
35561
35562 // 1) one cursor, no action
35563 let before = " abc\n abc\nabc\n ˇabc";
35564 cx.set_state(before);
35565 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35566 cx.assert_editor_state(before);
35567
35568 // 2) multiple cursors at different rows
35569 let before = indoc!(
35570 r#"
35571 let aˇbc = 123;
35572 let xˇyz = 456;
35573 let fˇoo = 789;
35574 let bˇar = 0;
35575 "#
35576 );
35577 let after = indoc!(
35578 r#"
35579 let a ˇbc = 123;
35580 let x ˇyz = 456;
35581 let f ˇoo = 789;
35582 let bˇar = 0;
35583 "#
35584 );
35585 cx.set_state(before);
35586 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35587 cx.assert_editor_state(after);
35588
35589 // 3) multiple selections at different rows
35590 let before = indoc!(
35591 r#"
35592 let «ˇabc» = 123;
35593 let «ˇxyz» = 456;
35594 let «ˇfoo» = 789;
35595 let «ˇbar» = 0;
35596 "#
35597 );
35598 let after = indoc!(
35599 r#"
35600 let «ˇabc» = 123;
35601 let «ˇxyz» = 456;
35602 let «ˇfoo» = 789;
35603 let «ˇbar» = 0;
35604 "#
35605 );
35606 cx.set_state(before);
35607 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35608 cx.assert_editor_state(after);
35609
35610 // 4) multiple selections at different rows, inverted head
35611 let before = indoc!(
35612 r#"
35613 let «abcˇ» = 123;
35614 // comment
35615 let «xyzˇ» = 456;
35616 let «fooˇ» = 789;
35617 let «barˇ» = 0;
35618 "#
35619 );
35620 let after = indoc!(
35621 r#"
35622 let «abcˇ» = 123;
35623 // comment
35624 let «xyzˇ» = 456;
35625 let «fooˇ» = 789;
35626 let «barˇ» = 0;
35627 "#
35628 );
35629 cx.set_state(before);
35630 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35631 cx.assert_editor_state(after);
35632}
35633
35634#[gpui::test]
35635async fn test_align_selections_multicolumn(cx: &mut TestAppContext) {
35636 init_test(cx, |_| {});
35637 let mut cx = EditorTestContext::new(cx).await;
35638
35639 // 1) Multicolumn, one non affected editor row
35640 let before = indoc!(
35641 r#"
35642 name «|ˇ» age «|ˇ» height «|ˇ» note
35643 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
35644 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
35645 Anything that is not selected
35646 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
35647 "#
35648 );
35649 let after = indoc!(
35650 r#"
35651 name «|ˇ» age «|ˇ» height «|ˇ» note
35652 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
35653 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
35654 Anything that is not selected
35655 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
35656 "#
35657 );
35658 cx.set_state(before);
35659 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35660 cx.assert_editor_state(after);
35661
35662 // 2) not all alignment rows has the number of alignment columns
35663 let before = indoc!(
35664 r#"
35665 name «|ˇ» age «|ˇ» height
35666 Matthew «|ˇ» 7 «|ˇ» 2333
35667 Mike «|ˇ» 1234
35668 Miles «|ˇ» 88 «|ˇ» 99
35669 "#
35670 );
35671 let after = indoc!(
35672 r#"
35673 name «|ˇ» age «|ˇ» height
35674 Matthew «|ˇ» 7 «|ˇ» 2333
35675 Mike «|ˇ» 1234
35676 Miles «|ˇ» 88 «|ˇ» 99
35677 "#
35678 );
35679 cx.set_state(before);
35680 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35681 cx.assert_editor_state(after);
35682
35683 // 3) A aligned column shall stay aligned
35684 let before = indoc!(
35685 r#"
35686 $ ˇa ˇa
35687 $ ˇa ˇa
35688 $ ˇa ˇa
35689 $ ˇa ˇa
35690 "#
35691 );
35692 let after = indoc!(
35693 r#"
35694 $ ˇa ˇa
35695 $ ˇa ˇa
35696 $ ˇa ˇa
35697 $ ˇa ˇa
35698 "#
35699 );
35700 cx.set_state(before);
35701 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
35702 cx.assert_editor_state(after);
35703}
35704
35705#[gpui::test]
35706async fn test_custom_fallback_highlights(cx: &mut TestAppContext) {
35707 init_test(cx, |_| {});
35708
35709 let mut cx = EditorTestContext::new(cx).await;
35710 cx.set_state(indoc! {"fn main(self, variable: TType) {ˇ}"});
35711
35712 let variable_color = Hsla::green();
35713 let function_color = Hsla::blue();
35714
35715 let test_cases = [
35716 ("@variable", Some(variable_color)),
35717 ("@type", None),
35718 ("@type @variable", Some(variable_color)),
35719 ("@variable @type", Some(variable_color)),
35720 ("@variable @function", Some(function_color)),
35721 ("@function @variable", Some(variable_color)),
35722 ];
35723
35724 for (test_case, expected) in test_cases {
35725 let custom_rust_lang = Arc::into_inner(rust_lang())
35726 .unwrap()
35727 .with_highlights_query(format! {r#"(type_identifier) {test_case}"#}.as_str())
35728 .unwrap();
35729 let theme = setup_syntax_highlighting(Arc::new(custom_rust_lang), &mut cx);
35730 let expected = expected.map_or_else(Vec::new, |expected_color| {
35731 vec![(24..29, HighlightStyle::color(expected_color))]
35732 });
35733
35734 cx.update_editor(|editor, window, cx| {
35735 let snapshot = editor.snapshot(window, cx);
35736 assert_eq!(
35737 expected,
35738 snapshot.combined_highlights(MultiBufferOffset(0)..snapshot.buffer().len(), &theme),
35739 "Test case with '{test_case}' highlights query did not pass",
35740 );
35741 });
35742 }
35743}
35744
35745fn setup_syntax_highlighting(
35746 language: Arc<Language>,
35747 cx: &mut EditorTestContext,
35748) -> Arc<SyntaxTheme> {
35749 let syntax = Arc::new(SyntaxTheme::new_test(vec![
35750 ("keyword", Hsla::red()),
35751 ("function", Hsla::blue()),
35752 ("variable", Hsla::green()),
35753 ("number", Hsla::default()),
35754 ("operator", Hsla::default()),
35755 ("punctuation.bracket", Hsla::default()),
35756 ("punctuation.delimiter", Hsla::default()),
35757 ]));
35758
35759 language.set_theme(&syntax);
35760
35761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
35762 cx.executor().run_until_parked();
35763 cx.update_editor(|editor, window, cx| {
35764 editor.set_style(
35765 EditorStyle {
35766 syntax: syntax.clone(),
35767 ..EditorStyle::default()
35768 },
35769 window,
35770 cx,
35771 );
35772 });
35773
35774 syntax
35775}