Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: split picker tests

RivoLink e054e91b 9f7b025b

+768 -761
+1 -761
src/tests/app.rs
··· 1 - use super::{test_assets, unique_temp_dir}; 1 + use super::test_assets; 2 2 use crate::app::{App, AppConfig, FileChange}; 3 3 use crate::cli::parse_cli; 4 4 use crate::markdown::{hash_str, parse_markdown, parse_markdown_with_width, read_file_state}; ··· 214 214 let app = App::new(lines, toc, "stdin".to_string(), false, false, None, None); 215 215 216 216 assert_eq!(app.active_highlight_line(), None); 217 - } 218 - 219 - #[test] 220 - fn file_picker_lists_dirs_then_markdown_files_only() { 221 - let unique = SystemTime::now() 222 - .duration_since(UNIX_EPOCH) 223 - .unwrap() 224 - .as_nanos(); 225 - let root = std::env::temp_dir().join(format!("leaf-picker-test-{unique}")); 226 - let _ = fs::remove_dir_all(&root); 227 - fs::create_dir_all(root.join("notes")).unwrap(); 228 - fs::write(root.join("README.md"), "# Demo\n").unwrap(); 229 - fs::write(root.join("draft.markdown"), "# Draft\n").unwrap(); 230 - fs::write(root.join("ignore.txt"), "nope\n").unwrap(); 231 - 232 - let mut app = App::new_with_source( 233 - Vec::new(), 234 - Vec::new(), 235 - AppConfig { 236 - filename: "picker".to_string(), 237 - source: String::new(), 238 - debug_input: false, 239 - watch: false, 240 - filepath: None, 241 - last_file_state: None, 242 - }, 243 - ); 244 - 245 - assert!(app.open_file_picker(root.clone())); 246 - 247 - let labels: Vec<_> = app 248 - .file_picker_entries() 249 - .iter() 250 - .map(|entry| entry.label()) 251 - .collect(); 252 - assert!(labels.contains(&"notes/")); 253 - assert!(labels.contains(&"README.md")); 254 - assert!(labels.contains(&"draft.markdown")); 255 - assert!(!labels.contains(&"ignore.txt")); 256 - 257 - let notes_idx = labels.iter().position(|label| *label == "notes/").unwrap(); 258 - let readme_idx = labels 259 - .iter() 260 - .position(|label| *label == "README.md") 261 - .unwrap(); 262 - assert!(notes_idx < readme_idx); 263 - 264 - let _ = fs::remove_dir_all(root); 265 - } 266 - 267 - #[test] 268 - fn fuzzy_file_picker_lists_markdown_files_from_subdirectories() { 269 - let unique = SystemTime::now() 270 - .duration_since(UNIX_EPOCH) 271 - .unwrap() 272 - .as_nanos(); 273 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-test-{unique}")); 274 - let _ = fs::remove_dir_all(&root); 275 - fs::create_dir_all(root.join("docs/nested")).unwrap(); 276 - fs::write(root.join("README.md"), "# Demo\n").unwrap(); 277 - fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 278 - fs::write(root.join("docs/nested/deep.markdown"), "# Deep\n").unwrap(); 279 - fs::write(root.join("ignore.txt"), "nope\n").unwrap(); 280 - 281 - let mut app = App::new_with_source( 282 - Vec::new(), 283 - Vec::new(), 284 - AppConfig { 285 - filename: "picker".to_string(), 286 - source: String::new(), 287 - debug_input: false, 288 - watch: false, 289 - filepath: None, 290 - last_file_state: None, 291 - }, 292 - ); 293 - 294 - assert!(app.open_fuzzy_file_picker(root.clone())); 295 - assert!(app.is_fuzzy_file_picker()); 296 - 297 - let labels: Vec<_> = app 298 - .file_picker_filtered_indices() 299 - .iter() 300 - .map(|idx| app.file_picker_entries()[*idx].label()) 301 - .collect(); 302 - assert!(labels.contains(&"README.md")); 303 - assert!(labels.contains(&"docs/guide.md")); 304 - assert!(labels.contains(&"docs/nested/deep.markdown")); 305 - assert!(!labels.contains(&"ignore.txt")); 306 - 307 - let _ = fs::remove_dir_all(root); 308 - } 309 - 310 - #[test] 311 - fn queued_fuzzy_picker_transitions_from_pending_to_loading_to_open() { 312 - let unique = SystemTime::now() 313 - .duration_since(UNIX_EPOCH) 314 - .unwrap() 315 - .as_nanos(); 316 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-queued-{unique}")); 317 - let _ = fs::remove_dir_all(&root); 318 - fs::create_dir_all(root.join("docs")).unwrap(); 319 - fs::write(root.join("README.md"), "# Demo\n").unwrap(); 320 - fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 321 - 322 - let mut app = App::new_with_source( 323 - Vec::new(), 324 - Vec::new(), 325 - AppConfig { 326 - filename: "picker".to_string(), 327 - source: String::new(), 328 - debug_input: false, 329 - watch: false, 330 - filepath: None, 331 - last_file_state: None, 332 - }, 333 - ); 334 - 335 - app.queue_fuzzy_file_picker(root.clone()); 336 - assert!(app.has_pending_picker()); 337 - assert_eq!( 338 - app.pending_picker_mode(), 339 - Some(crate::app::FilePickerMode::Fuzzy) 340 - ); 341 - assert_eq!(app.pending_picker_dir(), Some(root.as_path())); 342 - assert!(!app.is_picker_loading()); 343 - assert!(app.start_pending_picker_loading()); 344 - assert!(app.is_picker_loading()); 345 - app.age_picker_loading_by(std::time::Duration::from_secs(1)); 346 - let mut opened = false; 347 - for _ in 0..50 { 348 - if app.poll_picker_loading() { 349 - opened = app.is_file_picker_open(); 350 - break; 351 - } 352 - std::thread::sleep(std::time::Duration::from_millis(10)); 353 - } 354 - assert!(opened); 355 - assert!(app.is_file_picker_open()); 356 - assert!(app.is_fuzzy_file_picker()); 357 - assert!(!app.has_pending_picker()); 358 - assert!(!app.is_picker_loading()); 359 - 360 - let labels: Vec<_> = app 361 - .file_picker_filtered_indices() 362 - .iter() 363 - .map(|idx| app.file_picker_entries()[*idx].label()) 364 - .collect(); 365 - assert!(labels.contains(&"README.md")); 366 - assert!(labels.contains(&"docs/guide.md")); 367 - 368 - let _ = fs::remove_dir_all(root); 369 - } 370 - 371 - #[test] 372 - fn fuzzy_file_picker_uses_depth_first_order_with_hidden_first() { 373 - let unique = SystemTime::now() 374 - .duration_since(UNIX_EPOCH) 375 - .unwrap() 376 - .as_nanos(); 377 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-order-{unique}")); 378 - let _ = fs::remove_dir_all(&root); 379 - fs::create_dir_all(root.join(".private")).unwrap(); 380 - fs::create_dir_all(root.join("docs")).unwrap(); 381 - fs::write(root.join(".draft.md"), "# Hidden\n").unwrap(); 382 - fs::write(root.join(".private/alpha.md"), "# Private\n").unwrap(); 383 - fs::write(root.join("README.md"), "# Demo\n").unwrap(); 384 - fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 385 - 386 - let mut app = App::new_with_source( 387 - Vec::new(), 388 - Vec::new(), 389 - AppConfig { 390 - filename: "picker".to_string(), 391 - source: String::new(), 392 - debug_input: false, 393 - watch: false, 394 - filepath: None, 395 - last_file_state: None, 396 - }, 397 - ); 398 - 399 - assert!(app.open_fuzzy_file_picker(root.clone())); 400 - 401 - let labels: Vec<_> = app 402 - .file_picker_filtered_indices() 403 - .iter() 404 - .map(|idx| app.file_picker_entries()[*idx].label()) 405 - .collect(); 406 - assert_eq!( 407 - labels, 408 - vec![ 409 - ".draft.md", 410 - "README.md", 411 - ".private/alpha.md", 412 - "docs/guide.md", 413 - ] 414 - ); 415 - 416 - let _ = fs::remove_dir_all(root); 417 - } 418 - 419 - #[test] 420 - fn fuzzy_file_picker_uses_depth_first_file_order() { 421 - let unique = SystemTime::now() 422 - .duration_since(UNIX_EPOCH) 423 - .unwrap() 424 - .as_nanos(); 425 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-bfs-{unique}")); 426 - let _ = fs::remove_dir_all(&root); 427 - fs::create_dir_all(root.join("a/deep")).unwrap(); 428 - fs::create_dir_all(root.join("b")).unwrap(); 429 - fs::write(root.join("z-root.md"), "# Root\n").unwrap(); 430 - fs::write(root.join("a/a-child.md"), "# Child A\n").unwrap(); 431 - fs::write(root.join("b/b-child.md"), "# Child B\n").unwrap(); 432 - fs::write(root.join("a/deep/a-deep.md"), "# Deep\n").unwrap(); 433 - 434 - let mut app = App::new_with_source( 435 - Vec::new(), 436 - Vec::new(), 437 - AppConfig { 438 - filename: "picker".to_string(), 439 - source: String::new(), 440 - debug_input: false, 441 - watch: false, 442 - filepath: None, 443 - last_file_state: None, 444 - }, 445 - ); 446 - 447 - assert!(app.open_fuzzy_file_picker(root.clone())); 448 - 449 - let labels: Vec<_> = app 450 - .file_picker_filtered_indices() 451 - .iter() 452 - .map(|idx| app.file_picker_entries()[*idx].label()) 453 - .collect(); 454 - assert_eq!( 455 - labels, 456 - vec![ 457 - "z-root.md", 458 - "a/a-child.md", 459 - "a/deep/a-deep.md", 460 - "b/b-child.md" 461 - ] 462 - ); 463 - 464 - let _ = fs::remove_dir_all(root); 465 - } 466 - 467 - #[test] 468 - fn fuzzy_file_picker_keeps_depth_first_order_when_query_is_empty() { 469 - let unique = SystemTime::now() 470 - .duration_since(UNIX_EPOCH) 471 - .unwrap() 472 - .as_nanos(); 473 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-empty-query-{unique}")); 474 - let _ = fs::remove_dir_all(&root); 475 - fs::create_dir_all(root.join(".nvm")).unwrap(); 476 - fs::create_dir_all(root.join("projects")).unwrap(); 477 - fs::write(root.join(".nvm/README.md"), "# Hidden Readme\n").unwrap(); 478 - fs::write(root.join(".nvm/ROADMAP.md"), "# Hidden Roadmap\n").unwrap(); 479 - fs::write(root.join("projects/README.md"), "# Project Readme\n").unwrap(); 480 - 481 - let mut app = App::new_with_source( 482 - Vec::new(), 483 - Vec::new(), 484 - AppConfig { 485 - filename: "picker".to_string(), 486 - source: String::new(), 487 - debug_input: false, 488 - watch: false, 489 - filepath: None, 490 - last_file_state: None, 491 - }, 492 - ); 493 - 494 - assert!(app.open_fuzzy_file_picker(root.clone())); 495 - 496 - let labels: Vec<_> = app 497 - .file_picker_filtered_indices() 498 - .iter() 499 - .map(|idx| app.file_picker_entries()[*idx].label()) 500 - .collect(); 501 - assert_eq!( 502 - labels, 503 - vec![".nvm/README.md", ".nvm/ROADMAP.md", "projects/README.md"] 504 - ); 505 - 506 - let _ = fs::remove_dir_all(root); 507 - } 508 - 509 - #[test] 510 - fn fuzzy_file_picker_filters_entries_by_query() { 511 - let unique = SystemTime::now() 512 - .duration_since(UNIX_EPOCH) 513 - .unwrap() 514 - .as_nanos(); 515 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-query-{unique}")); 516 - let _ = fs::remove_dir_all(&root); 517 - fs::create_dir_all(root.join("docs")).unwrap(); 518 - fs::write(root.join("README.md"), "# Demo\n").unwrap(); 519 - fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 520 - 521 - let mut app = App::new_with_source( 522 - Vec::new(), 523 - Vec::new(), 524 - AppConfig { 525 - filename: "picker".to_string(), 526 - source: String::new(), 527 - debug_input: false, 528 - watch: false, 529 - filepath: None, 530 - last_file_state: None, 531 - }, 532 - ); 533 - 534 - assert!(app.open_fuzzy_file_picker(root.clone())); 535 - app.push_file_picker_query('g'); 536 - app.push_file_picker_query('u'); 537 - 538 - let labels: Vec<_> = app 539 - .file_picker_filtered_indices() 540 - .iter() 541 - .map(|idx| app.file_picker_entries()[*idx].label()) 542 - .collect(); 543 - assert_eq!(labels, vec!["docs/guide.md"]); 544 - 545 - let _ = fs::remove_dir_all(root); 546 - } 547 - 548 - #[test] 549 - fn fuzzy_file_picker_does_not_match_directory_segments() { 550 - let unique = SystemTime::now() 551 - .duration_since(UNIX_EPOCH) 552 - .unwrap() 553 - .as_nanos(); 554 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-cla-{unique}")); 555 - let _ = fs::remove_dir_all(&root); 556 - fs::create_dir_all(root.join(".notes/backup")).unwrap(); 557 - fs::write(root.join(".notes/backup/PLAN.md"), "# Plan\n").unwrap(); 558 - fs::write(root.join("claude.md"), "# Claude\n").unwrap(); 559 - 560 - let mut app = App::new_with_source( 561 - Vec::new(), 562 - Vec::new(), 563 - AppConfig { 564 - filename: "picker".to_string(), 565 - source: String::new(), 566 - debug_input: false, 567 - watch: false, 568 - filepath: None, 569 - last_file_state: None, 570 - }, 571 - ); 572 - 573 - assert!(app.open_fuzzy_file_picker(root.clone())); 574 - app.push_file_picker_query('c'); 575 - app.push_file_picker_query('l'); 576 - app.push_file_picker_query('a'); 577 - 578 - let labels: Vec<_> = app 579 - .file_picker_filtered_indices() 580 - .iter() 581 - .map(|idx| app.file_picker_entries()[*idx].label()) 582 - .collect(); 583 - assert!(labels.contains(&"claude.md")); 584 - assert!(!labels.contains(&".notes/backup/PLAN.md")); 585 - 586 - let _ = fs::remove_dir_all(root); 587 - } 588 - 589 - #[test] 590 - fn fuzzy_file_picker_tracks_match_positions_for_highlighting() { 591 - let unique = SystemTime::now() 592 - .duration_since(UNIX_EPOCH) 593 - .unwrap() 594 - .as_nanos(); 595 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-highlight-{unique}")); 596 - let _ = fs::remove_dir_all(&root); 597 - fs::create_dir_all(&root).unwrap(); 598 - fs::write(root.join("claude.md"), "# Claude\n").unwrap(); 599 - 600 - let mut app = App::new_with_source( 601 - Vec::new(), 602 - Vec::new(), 603 - AppConfig { 604 - filename: "picker".to_string(), 605 - source: String::new(), 606 - debug_input: false, 607 - watch: false, 608 - filepath: None, 609 - last_file_state: None, 610 - }, 611 - ); 612 - 613 - assert!(app.open_fuzzy_file_picker(root.clone())); 614 - app.push_file_picker_query('c'); 615 - app.push_file_picker_query('l'); 616 - app.push_file_picker_query('a'); 617 - 618 - let labels: Vec<_> = app 619 - .file_picker_filtered_indices() 620 - .iter() 621 - .map(|idx| app.file_picker_entries()[*idx].label()) 622 - .collect(); 623 - assert_eq!(labels, vec!["claude.md"]); 624 - assert_eq!(app.file_picker_match_positions(0), &[0, 1, 2]); 625 - 626 - let _ = fs::remove_dir_all(root); 627 - } 628 - 629 - #[test] 630 - fn fuzzy_file_picker_prefers_compact_matches() { 631 - let unique = SystemTime::now() 632 - .duration_since(UNIX_EPOCH) 633 - .unwrap() 634 - .as_nanos(); 635 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-compact-{unique}")); 636 - let _ = fs::remove_dir_all(&root); 637 - fs::create_dir_all(&root).unwrap(); 638 - fs::write(root.join("case.md"), "# Case\n").unwrap(); 639 - fs::write(root.join("ciase.md"), "# Ciase\n").unwrap(); 640 - 641 - let mut app = App::new_with_source( 642 - Vec::new(), 643 - Vec::new(), 644 - AppConfig { 645 - filename: "picker".to_string(), 646 - source: String::new(), 647 - debug_input: false, 648 - watch: false, 649 - filepath: None, 650 - last_file_state: None, 651 - }, 652 - ); 653 - 654 - assert!(app.open_fuzzy_file_picker(root.clone())); 655 - app.push_file_picker_query('c'); 656 - app.push_file_picker_query('a'); 657 - 658 - let labels: Vec<_> = app 659 - .file_picker_filtered_indices() 660 - .iter() 661 - .map(|idx| app.file_picker_entries()[*idx].label()) 662 - .collect(); 663 - assert_eq!(labels, vec!["case.md", "ciase.md"]); 664 - 665 - let _ = fs::remove_dir_all(root); 666 - } 667 - 668 - #[test] 669 - fn fuzzy_file_picker_prefers_contiguous_matches_over_earlier_scattered_matches() { 670 - let unique = SystemTime::now() 671 - .duration_since(UNIX_EPOCH) 672 - .unwrap() 673 - .as_nanos(); 674 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-contiguous-{unique}")); 675 - let _ = fs::remove_dir_all(&root); 676 - fs::create_dir_all(root.join(".notes/todo")).unwrap(); 677 - fs::create_dir_all(root.join(".notes/tests")).unwrap(); 678 - fs::write(root.join(".notes/todo/review-chatgpt.md"), "# ChatGPT\n").unwrap(); 679 - fs::write(root.join(".notes/tests/themes-showcase.md"), "# Showcase\n").unwrap(); 680 - 681 - let mut app = App::new_with_source( 682 - Vec::new(), 683 - Vec::new(), 684 - AppConfig { 685 - filename: "picker".to_string(), 686 - source: String::new(), 687 - debug_input: false, 688 - watch: false, 689 - filepath: None, 690 - last_file_state: None, 691 - }, 692 - ); 693 - 694 - assert!(app.open_fuzzy_file_picker(root.clone())); 695 - app.push_file_picker_query('c'); 696 - app.push_file_picker_query('a'); 697 - 698 - let labels: Vec<_> = app 699 - .file_picker_filtered_indices() 700 - .iter() 701 - .map(|idx| app.file_picker_entries()[*idx].label()) 702 - .collect(); 703 - let showcase_idx = labels 704 - .iter() 705 - .position(|label| *label == ".notes/tests/themes-showcase.md") 706 - .unwrap(); 707 - let chatgpt_idx = labels 708 - .iter() 709 - .position(|label| *label == ".notes/todo/review-chatgpt.md") 710 - .unwrap(); 711 - assert!(showcase_idx < chatgpt_idx); 712 - 713 - let _ = fs::remove_dir_all(root); 714 - } 715 - 716 - #[test] 717 - fn fuzzy_file_picker_prefers_filename_prefix_matches() { 718 - let unique = SystemTime::now() 719 - .duration_since(UNIX_EPOCH) 720 - .unwrap() 721 - .as_nanos(); 722 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-prefix-{unique}")); 723 - let _ = fs::remove_dir_all(&root); 724 - fs::create_dir_all(&root).unwrap(); 725 - fs::write(root.join("todo-case.md"), "# Todo\n").unwrap(); 726 - fs::write(root.join("case-study.md"), "# Case\n").unwrap(); 727 - 728 - let mut app = App::new_with_source( 729 - Vec::new(), 730 - Vec::new(), 731 - AppConfig { 732 - filename: "picker".to_string(), 733 - source: String::new(), 734 - debug_input: false, 735 - watch: false, 736 - filepath: None, 737 - last_file_state: None, 738 - }, 739 - ); 740 - 741 - assert!(app.open_fuzzy_file_picker(root.clone())); 742 - app.push_file_picker_query('c'); 743 - app.push_file_picker_query('a'); 744 - 745 - let labels: Vec<_> = app 746 - .file_picker_filtered_indices() 747 - .iter() 748 - .map(|idx| app.file_picker_entries()[*idx].label()) 749 - .collect(); 750 - assert_eq!(labels, vec!["case-study.md", "todo-case.md"]); 751 - 752 - let _ = fs::remove_dir_all(root); 753 - } 754 - 755 - #[test] 756 - fn fuzzy_file_picker_prefers_token_boundary_matches() { 757 - let unique = SystemTime::now() 758 - .duration_since(UNIX_EPOCH) 759 - .unwrap() 760 - .as_nanos(); 761 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-boundary-{unique}")); 762 - let _ = fs::remove_dir_all(&root); 763 - fs::create_dir_all(&root).unwrap(); 764 - fs::write(root.join("alpha-case.md"), "# Boundary\n").unwrap(); 765 - fs::write(root.join("alphacase.md"), "# Plain\n").unwrap(); 766 - 767 - let mut app = App::new_with_source( 768 - Vec::new(), 769 - Vec::new(), 770 - AppConfig { 771 - filename: "picker".to_string(), 772 - source: String::new(), 773 - debug_input: false, 774 - watch: false, 775 - filepath: None, 776 - last_file_state: None, 777 - }, 778 - ); 779 - 780 - assert!(app.open_fuzzy_file_picker(root.clone())); 781 - app.push_file_picker_query('c'); 782 - app.push_file_picker_query('a'); 783 - 784 - let labels: Vec<_> = app 785 - .file_picker_filtered_indices() 786 - .iter() 787 - .map(|idx| app.file_picker_entries()[*idx].label()) 788 - .collect(); 789 - assert_eq!(labels, vec!["alpha-case.md", "alphacase.md"]); 790 - 791 - let _ = fs::remove_dir_all(root); 792 - } 793 - 794 - #[test] 795 - fn fuzzy_file_picker_prefers_shallower_paths_on_equal_scores() { 796 - let unique = SystemTime::now() 797 - .duration_since(UNIX_EPOCH) 798 - .unwrap() 799 - .as_nanos(); 800 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-depth-{unique}")); 801 - let _ = fs::remove_dir_all(&root); 802 - fs::create_dir_all(root.join("nested/deeper")).unwrap(); 803 - fs::write(root.join("case.md"), "# Root\n").unwrap(); 804 - fs::write(root.join("nested/deeper/case.md"), "# Nested\n").unwrap(); 805 - 806 - let mut app = App::new_with_source( 807 - Vec::new(), 808 - Vec::new(), 809 - AppConfig { 810 - filename: "picker".to_string(), 811 - source: String::new(), 812 - debug_input: false, 813 - watch: false, 814 - filepath: None, 815 - last_file_state: None, 816 - }, 817 - ); 818 - 819 - assert!(app.open_fuzzy_file_picker(root.clone())); 820 - app.push_file_picker_query('c'); 821 - app.push_file_picker_query('a'); 822 - app.push_file_picker_query('s'); 823 - app.push_file_picker_query('e'); 824 - 825 - let labels: Vec<_> = app 826 - .file_picker_filtered_indices() 827 - .iter() 828 - .map(|idx| app.file_picker_entries()[*idx].label()) 829 - .collect(); 830 - assert_eq!(labels, vec!["case.md", "nested/deeper/case.md"]); 831 - 832 - let _ = fs::remove_dir_all(root); 833 - } 834 - 835 - #[test] 836 - fn fuzzy_file_picker_allows_q_in_query() { 837 - let unique = SystemTime::now() 838 - .duration_since(UNIX_EPOCH) 839 - .unwrap() 840 - .as_nanos(); 841 - let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-q-{unique}")); 842 - let _ = fs::remove_dir_all(&root); 843 - fs::create_dir_all(&root).unwrap(); 844 - fs::write(root.join("query.md"), "# Query\n").unwrap(); 845 - 846 - let mut app = App::new_with_source( 847 - Vec::new(), 848 - Vec::new(), 849 - AppConfig { 850 - filename: "picker".to_string(), 851 - source: String::new(), 852 - debug_input: false, 853 - watch: false, 854 - filepath: None, 855 - last_file_state: None, 856 - }, 857 - ); 858 - 859 - assert!(app.open_fuzzy_file_picker(root.clone())); 860 - app.push_file_picker_query('q'); 861 - assert_eq!(app.file_picker_query(), "q"); 862 - 863 - let labels: Vec<_> = app 864 - .file_picker_filtered_indices() 865 - .iter() 866 - .map(|idx| app.file_picker_entries()[*idx].label()) 867 - .collect(); 868 - assert_eq!(labels, vec!["query.md"]); 869 - 870 - let _ = fs::remove_dir_all(root); 871 - } 872 - 873 - #[test] 874 - fn fuzzy_file_picker_skips_ignored_technical_directories() { 875 - let root = unique_temp_dir("leaf-fuzzy-picker-ignore"); 876 - let _ = fs::remove_dir_all(&root); 877 - fs::create_dir_all(root.join(".git")).unwrap(); 878 - fs::create_dir_all(root.join("target")).unwrap(); 879 - fs::create_dir_all(root.join("vendor")).unwrap(); 880 - fs::create_dir_all(root.join("var")).unwrap(); 881 - fs::create_dir_all(root.join(".notes")).unwrap(); 882 - fs::write(root.join(".git/ignored.md"), "# Ignored\n").unwrap(); 883 - fs::write(root.join("target/ignored.md"), "# Ignored\n").unwrap(); 884 - fs::write(root.join("vendor/ignored.md"), "# Ignored\n").unwrap(); 885 - fs::write(root.join("var/ignored.md"), "# Ignored\n").unwrap(); 886 - fs::write(root.join(".notes/kept.md"), "# Kept\n").unwrap(); 887 - 888 - let mut app = App::new_with_source( 889 - Vec::new(), 890 - Vec::new(), 891 - AppConfig { 892 - filename: "picker".to_string(), 893 - source: String::new(), 894 - debug_input: false, 895 - watch: false, 896 - filepath: None, 897 - last_file_state: None, 898 - }, 899 - ); 900 - 901 - assert!(app.open_fuzzy_file_picker(root.clone())); 902 - 903 - let labels: Vec<_> = app 904 - .file_picker_filtered_indices() 905 - .iter() 906 - .map(|idx| app.file_picker_entries()[*idx].label()) 907 - .collect(); 908 - assert_eq!(labels, vec![".notes/kept.md"]); 909 - assert_eq!(app.file_picker_truncation(), None); 910 - 911 - let _ = fs::remove_dir_all(root); 912 - } 913 - 914 - #[test] 915 - fn fuzzy_file_picker_reports_directory_limit_truncation() { 916 - let root = unique_temp_dir("leaf-fuzzy-picker-dir-limit"); 917 - let _ = fs::remove_dir_all(&root); 918 - for idx in 0..5_050usize { 919 - let dir = root.join(format!("nested-{idx:04}")); 920 - fs::create_dir_all(&dir).unwrap(); 921 - fs::write(dir.join(format!("file-{idx:04}.md")), "# File\n").unwrap(); 922 - } 923 - 924 - let mut app = App::new_with_source( 925 - Vec::new(), 926 - Vec::new(), 927 - AppConfig { 928 - filename: "picker".to_string(), 929 - source: String::new(), 930 - debug_input: false, 931 - watch: false, 932 - filepath: None, 933 - last_file_state: None, 934 - }, 935 - ); 936 - 937 - assert!(app.open_fuzzy_file_picker(root.clone())); 938 - assert_eq!( 939 - app.file_picker_truncation(), 940 - Some(crate::app::PickerIndexTruncation::Directory) 941 - ); 942 - assert!(!app.file_picker_entries().is_empty()); 943 - 944 - let _ = fs::remove_dir_all(root); 945 - } 946 - 947 - #[test] 948 - fn fuzzy_file_picker_reports_file_limit_truncation() { 949 - let root = unique_temp_dir("leaf-fuzzy-picker-file-limit"); 950 - let _ = fs::remove_dir_all(&root); 951 - fs::create_dir_all(&root).unwrap(); 952 - for idx in 0..10_050usize { 953 - fs::write(root.join(format!("file-{idx:05}.md")), "# File\n").unwrap(); 954 - } 955 - 956 - let mut app = App::new_with_source( 957 - Vec::new(), 958 - Vec::new(), 959 - AppConfig { 960 - filename: "picker".to_string(), 961 - source: String::new(), 962 - debug_input: false, 963 - watch: false, 964 - filepath: None, 965 - last_file_state: None, 966 - }, 967 - ); 968 - 969 - assert!(app.open_fuzzy_file_picker(root.clone())); 970 - assert_eq!( 971 - app.file_picker_truncation(), 972 - Some(crate::app::PickerIndexTruncation::File) 973 - ); 974 - assert_eq!(app.file_picker_entries().len(), 10_000); 975 - 976 - let _ = fs::remove_dir_all(root); 977 217 } 978 218 979 219 #[test]
+766
src/tests/file_picker.rs
··· 1 + use super::unique_temp_dir; 2 + use crate::app::{App, AppConfig}; 3 + use std::{ 4 + fs, 5 + time::{SystemTime, UNIX_EPOCH}, 6 + }; 7 + 8 + #[test] 9 + fn file_picker_lists_dirs_then_markdown_files_only() { 10 + let unique = SystemTime::now() 11 + .duration_since(UNIX_EPOCH) 12 + .unwrap() 13 + .as_nanos(); 14 + let root = std::env::temp_dir().join(format!("leaf-picker-test-{unique}")); 15 + let _ = fs::remove_dir_all(&root); 16 + fs::create_dir_all(root.join("notes")).unwrap(); 17 + fs::write(root.join("README.md"), "# Demo\n").unwrap(); 18 + fs::write(root.join("draft.markdown"), "# Draft\n").unwrap(); 19 + fs::write(root.join("ignore.txt"), "nope\n").unwrap(); 20 + 21 + let mut app = App::new_with_source( 22 + Vec::new(), 23 + Vec::new(), 24 + AppConfig { 25 + filename: "picker".to_string(), 26 + source: String::new(), 27 + debug_input: false, 28 + watch: false, 29 + filepath: None, 30 + last_file_state: None, 31 + }, 32 + ); 33 + 34 + assert!(app.open_file_picker(root.clone())); 35 + 36 + let labels: Vec<_> = app 37 + .file_picker_entries() 38 + .iter() 39 + .map(|entry| entry.label()) 40 + .collect(); 41 + assert!(labels.contains(&"notes/")); 42 + assert!(labels.contains(&"README.md")); 43 + assert!(labels.contains(&"draft.markdown")); 44 + assert!(!labels.contains(&"ignore.txt")); 45 + 46 + let notes_idx = labels.iter().position(|label| *label == "notes/").unwrap(); 47 + let readme_idx = labels 48 + .iter() 49 + .position(|label| *label == "README.md") 50 + .unwrap(); 51 + assert!(notes_idx < readme_idx); 52 + 53 + let _ = fs::remove_dir_all(root); 54 + } 55 + 56 + #[test] 57 + fn fuzzy_file_picker_lists_markdown_files_from_subdirectories() { 58 + let unique = SystemTime::now() 59 + .duration_since(UNIX_EPOCH) 60 + .unwrap() 61 + .as_nanos(); 62 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-test-{unique}")); 63 + let _ = fs::remove_dir_all(&root); 64 + fs::create_dir_all(root.join("docs/nested")).unwrap(); 65 + fs::write(root.join("README.md"), "# Demo\n").unwrap(); 66 + fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 67 + fs::write(root.join("docs/nested/deep.markdown"), "# Deep\n").unwrap(); 68 + fs::write(root.join("ignore.txt"), "nope\n").unwrap(); 69 + 70 + let mut app = App::new_with_source( 71 + Vec::new(), 72 + Vec::new(), 73 + AppConfig { 74 + filename: "picker".to_string(), 75 + source: String::new(), 76 + debug_input: false, 77 + watch: false, 78 + filepath: None, 79 + last_file_state: None, 80 + }, 81 + ); 82 + 83 + assert!(app.open_fuzzy_file_picker(root.clone())); 84 + assert!(app.is_fuzzy_file_picker()); 85 + 86 + let labels: Vec<_> = app 87 + .file_picker_filtered_indices() 88 + .iter() 89 + .map(|idx| app.file_picker_entries()[*idx].label()) 90 + .collect(); 91 + assert!(labels.contains(&"README.md")); 92 + assert!(labels.contains(&"docs/guide.md")); 93 + assert!(labels.contains(&"docs/nested/deep.markdown")); 94 + assert!(!labels.contains(&"ignore.txt")); 95 + 96 + let _ = fs::remove_dir_all(root); 97 + } 98 + 99 + #[test] 100 + fn queued_fuzzy_picker_transitions_from_pending_to_loading_to_open() { 101 + let unique = SystemTime::now() 102 + .duration_since(UNIX_EPOCH) 103 + .unwrap() 104 + .as_nanos(); 105 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-queued-{unique}")); 106 + let _ = fs::remove_dir_all(&root); 107 + fs::create_dir_all(root.join("docs")).unwrap(); 108 + fs::write(root.join("README.md"), "# Demo\n").unwrap(); 109 + fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 110 + 111 + let mut app = App::new_with_source( 112 + Vec::new(), 113 + Vec::new(), 114 + AppConfig { 115 + filename: "picker".to_string(), 116 + source: String::new(), 117 + debug_input: false, 118 + watch: false, 119 + filepath: None, 120 + last_file_state: None, 121 + }, 122 + ); 123 + 124 + app.queue_fuzzy_file_picker(root.clone()); 125 + assert!(app.has_pending_picker()); 126 + assert_eq!( 127 + app.pending_picker_mode(), 128 + Some(crate::app::FilePickerMode::Fuzzy) 129 + ); 130 + assert_eq!(app.pending_picker_dir(), Some(root.as_path())); 131 + assert!(!app.is_picker_loading()); 132 + assert!(app.start_pending_picker_loading()); 133 + assert!(app.is_picker_loading()); 134 + app.age_picker_loading_by(std::time::Duration::from_secs(1)); 135 + let mut opened = false; 136 + for _ in 0..50 { 137 + if app.poll_picker_loading() { 138 + opened = app.is_file_picker_open(); 139 + break; 140 + } 141 + std::thread::sleep(std::time::Duration::from_millis(10)); 142 + } 143 + assert!(opened); 144 + assert!(app.is_file_picker_open()); 145 + assert!(app.is_fuzzy_file_picker()); 146 + assert!(!app.has_pending_picker()); 147 + assert!(!app.is_picker_loading()); 148 + 149 + let labels: Vec<_> = app 150 + .file_picker_filtered_indices() 151 + .iter() 152 + .map(|idx| app.file_picker_entries()[*idx].label()) 153 + .collect(); 154 + assert!(labels.contains(&"README.md")); 155 + assert!(labels.contains(&"docs/guide.md")); 156 + 157 + let _ = fs::remove_dir_all(root); 158 + } 159 + 160 + #[test] 161 + fn fuzzy_file_picker_uses_depth_first_order_with_hidden_first() { 162 + let unique = SystemTime::now() 163 + .duration_since(UNIX_EPOCH) 164 + .unwrap() 165 + .as_nanos(); 166 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-order-{unique}")); 167 + let _ = fs::remove_dir_all(&root); 168 + fs::create_dir_all(root.join(".private")).unwrap(); 169 + fs::create_dir_all(root.join("docs")).unwrap(); 170 + fs::write(root.join(".draft.md"), "# Hidden\n").unwrap(); 171 + fs::write(root.join(".private/alpha.md"), "# Private\n").unwrap(); 172 + fs::write(root.join("README.md"), "# Demo\n").unwrap(); 173 + fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 174 + 175 + let mut app = App::new_with_source( 176 + Vec::new(), 177 + Vec::new(), 178 + AppConfig { 179 + filename: "picker".to_string(), 180 + source: String::new(), 181 + debug_input: false, 182 + watch: false, 183 + filepath: None, 184 + last_file_state: None, 185 + }, 186 + ); 187 + 188 + assert!(app.open_fuzzy_file_picker(root.clone())); 189 + 190 + let labels: Vec<_> = app 191 + .file_picker_filtered_indices() 192 + .iter() 193 + .map(|idx| app.file_picker_entries()[*idx].label()) 194 + .collect(); 195 + assert_eq!( 196 + labels, 197 + vec![ 198 + ".draft.md", 199 + "README.md", 200 + ".private/alpha.md", 201 + "docs/guide.md", 202 + ] 203 + ); 204 + 205 + let _ = fs::remove_dir_all(root); 206 + } 207 + 208 + #[test] 209 + fn fuzzy_file_picker_uses_depth_first_file_order() { 210 + let unique = SystemTime::now() 211 + .duration_since(UNIX_EPOCH) 212 + .unwrap() 213 + .as_nanos(); 214 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-bfs-{unique}")); 215 + let _ = fs::remove_dir_all(&root); 216 + fs::create_dir_all(root.join("a/deep")).unwrap(); 217 + fs::create_dir_all(root.join("b")).unwrap(); 218 + fs::write(root.join("z-root.md"), "# Root\n").unwrap(); 219 + fs::write(root.join("a/a-child.md"), "# Child A\n").unwrap(); 220 + fs::write(root.join("b/b-child.md"), "# Child B\n").unwrap(); 221 + fs::write(root.join("a/deep/a-deep.md"), "# Deep\n").unwrap(); 222 + 223 + let mut app = App::new_with_source( 224 + Vec::new(), 225 + Vec::new(), 226 + AppConfig { 227 + filename: "picker".to_string(), 228 + source: String::new(), 229 + debug_input: false, 230 + watch: false, 231 + filepath: None, 232 + last_file_state: None, 233 + }, 234 + ); 235 + 236 + assert!(app.open_fuzzy_file_picker(root.clone())); 237 + 238 + let labels: Vec<_> = app 239 + .file_picker_filtered_indices() 240 + .iter() 241 + .map(|idx| app.file_picker_entries()[*idx].label()) 242 + .collect(); 243 + assert_eq!( 244 + labels, 245 + vec![ 246 + "z-root.md", 247 + "a/a-child.md", 248 + "a/deep/a-deep.md", 249 + "b/b-child.md" 250 + ] 251 + ); 252 + 253 + let _ = fs::remove_dir_all(root); 254 + } 255 + 256 + #[test] 257 + fn fuzzy_file_picker_keeps_depth_first_order_when_query_is_empty() { 258 + let unique = SystemTime::now() 259 + .duration_since(UNIX_EPOCH) 260 + .unwrap() 261 + .as_nanos(); 262 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-empty-query-{unique}")); 263 + let _ = fs::remove_dir_all(&root); 264 + fs::create_dir_all(root.join(".nvm")).unwrap(); 265 + fs::create_dir_all(root.join("projects")).unwrap(); 266 + fs::write(root.join(".nvm/README.md"), "# Hidden Readme\n").unwrap(); 267 + fs::write(root.join(".nvm/ROADMAP.md"), "# Hidden Roadmap\n").unwrap(); 268 + fs::write(root.join("projects/README.md"), "# Project Readme\n").unwrap(); 269 + 270 + let mut app = App::new_with_source( 271 + Vec::new(), 272 + Vec::new(), 273 + AppConfig { 274 + filename: "picker".to_string(), 275 + source: String::new(), 276 + debug_input: false, 277 + watch: false, 278 + filepath: None, 279 + last_file_state: None, 280 + }, 281 + ); 282 + 283 + assert!(app.open_fuzzy_file_picker(root.clone())); 284 + 285 + let labels: Vec<_> = app 286 + .file_picker_filtered_indices() 287 + .iter() 288 + .map(|idx| app.file_picker_entries()[*idx].label()) 289 + .collect(); 290 + assert_eq!( 291 + labels, 292 + vec![".nvm/README.md", ".nvm/ROADMAP.md", "projects/README.md"] 293 + ); 294 + 295 + let _ = fs::remove_dir_all(root); 296 + } 297 + 298 + #[test] 299 + fn fuzzy_file_picker_filters_entries_by_query() { 300 + let unique = SystemTime::now() 301 + .duration_since(UNIX_EPOCH) 302 + .unwrap() 303 + .as_nanos(); 304 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-query-{unique}")); 305 + let _ = fs::remove_dir_all(&root); 306 + fs::create_dir_all(root.join("docs")).unwrap(); 307 + fs::write(root.join("README.md"), "# Demo\n").unwrap(); 308 + fs::write(root.join("docs/guide.md"), "# Guide\n").unwrap(); 309 + 310 + let mut app = App::new_with_source( 311 + Vec::new(), 312 + Vec::new(), 313 + AppConfig { 314 + filename: "picker".to_string(), 315 + source: String::new(), 316 + debug_input: false, 317 + watch: false, 318 + filepath: None, 319 + last_file_state: None, 320 + }, 321 + ); 322 + 323 + assert!(app.open_fuzzy_file_picker(root.clone())); 324 + app.push_file_picker_query('g'); 325 + app.push_file_picker_query('u'); 326 + 327 + let labels: Vec<_> = app 328 + .file_picker_filtered_indices() 329 + .iter() 330 + .map(|idx| app.file_picker_entries()[*idx].label()) 331 + .collect(); 332 + assert_eq!(labels, vec!["docs/guide.md"]); 333 + 334 + let _ = fs::remove_dir_all(root); 335 + } 336 + 337 + #[test] 338 + fn fuzzy_file_picker_does_not_match_directory_segments() { 339 + let unique = SystemTime::now() 340 + .duration_since(UNIX_EPOCH) 341 + .unwrap() 342 + .as_nanos(); 343 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-cla-{unique}")); 344 + let _ = fs::remove_dir_all(&root); 345 + fs::create_dir_all(root.join(".notes/backup")).unwrap(); 346 + fs::write(root.join(".notes/backup/PLAN.md"), "# Plan\n").unwrap(); 347 + fs::write(root.join("claude.md"), "# Claude\n").unwrap(); 348 + 349 + let mut app = App::new_with_source( 350 + Vec::new(), 351 + Vec::new(), 352 + AppConfig { 353 + filename: "picker".to_string(), 354 + source: String::new(), 355 + debug_input: false, 356 + watch: false, 357 + filepath: None, 358 + last_file_state: None, 359 + }, 360 + ); 361 + 362 + assert!(app.open_fuzzy_file_picker(root.clone())); 363 + app.push_file_picker_query('c'); 364 + app.push_file_picker_query('l'); 365 + app.push_file_picker_query('a'); 366 + 367 + let labels: Vec<_> = app 368 + .file_picker_filtered_indices() 369 + .iter() 370 + .map(|idx| app.file_picker_entries()[*idx].label()) 371 + .collect(); 372 + assert!(labels.contains(&"claude.md")); 373 + assert!(!labels.contains(&".notes/backup/PLAN.md")); 374 + 375 + let _ = fs::remove_dir_all(root); 376 + } 377 + 378 + #[test] 379 + fn fuzzy_file_picker_tracks_match_positions_for_highlighting() { 380 + let unique = SystemTime::now() 381 + .duration_since(UNIX_EPOCH) 382 + .unwrap() 383 + .as_nanos(); 384 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-highlight-{unique}")); 385 + let _ = fs::remove_dir_all(&root); 386 + fs::create_dir_all(&root).unwrap(); 387 + fs::write(root.join("claude.md"), "# Claude\n").unwrap(); 388 + 389 + let mut app = App::new_with_source( 390 + Vec::new(), 391 + Vec::new(), 392 + AppConfig { 393 + filename: "picker".to_string(), 394 + source: String::new(), 395 + debug_input: false, 396 + watch: false, 397 + filepath: None, 398 + last_file_state: None, 399 + }, 400 + ); 401 + 402 + assert!(app.open_fuzzy_file_picker(root.clone())); 403 + app.push_file_picker_query('c'); 404 + app.push_file_picker_query('l'); 405 + app.push_file_picker_query('a'); 406 + 407 + let labels: Vec<_> = app 408 + .file_picker_filtered_indices() 409 + .iter() 410 + .map(|idx| app.file_picker_entries()[*idx].label()) 411 + .collect(); 412 + assert_eq!(labels, vec!["claude.md"]); 413 + assert_eq!(app.file_picker_match_positions(0), &[0, 1, 2]); 414 + 415 + let _ = fs::remove_dir_all(root); 416 + } 417 + 418 + #[test] 419 + fn fuzzy_file_picker_prefers_compact_matches() { 420 + let unique = SystemTime::now() 421 + .duration_since(UNIX_EPOCH) 422 + .unwrap() 423 + .as_nanos(); 424 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-compact-{unique}")); 425 + let _ = fs::remove_dir_all(&root); 426 + fs::create_dir_all(&root).unwrap(); 427 + fs::write(root.join("case.md"), "# Case\n").unwrap(); 428 + fs::write(root.join("ciase.md"), "# Ciase\n").unwrap(); 429 + 430 + let mut app = App::new_with_source( 431 + Vec::new(), 432 + Vec::new(), 433 + AppConfig { 434 + filename: "picker".to_string(), 435 + source: String::new(), 436 + debug_input: false, 437 + watch: false, 438 + filepath: None, 439 + last_file_state: None, 440 + }, 441 + ); 442 + 443 + assert!(app.open_fuzzy_file_picker(root.clone())); 444 + app.push_file_picker_query('c'); 445 + app.push_file_picker_query('a'); 446 + 447 + let labels: Vec<_> = app 448 + .file_picker_filtered_indices() 449 + .iter() 450 + .map(|idx| app.file_picker_entries()[*idx].label()) 451 + .collect(); 452 + assert_eq!(labels, vec!["case.md", "ciase.md"]); 453 + 454 + let _ = fs::remove_dir_all(root); 455 + } 456 + 457 + #[test] 458 + fn fuzzy_file_picker_prefers_contiguous_matches_over_earlier_scattered_matches() { 459 + let unique = SystemTime::now() 460 + .duration_since(UNIX_EPOCH) 461 + .unwrap() 462 + .as_nanos(); 463 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-contiguous-{unique}")); 464 + let _ = fs::remove_dir_all(&root); 465 + fs::create_dir_all(root.join(".notes/todo")).unwrap(); 466 + fs::create_dir_all(root.join(".notes/tests")).unwrap(); 467 + fs::write(root.join(".notes/todo/review-chatgpt.md"), "# ChatGPT\n").unwrap(); 468 + fs::write(root.join(".notes/tests/themes-showcase.md"), "# Showcase\n").unwrap(); 469 + 470 + let mut app = App::new_with_source( 471 + Vec::new(), 472 + Vec::new(), 473 + AppConfig { 474 + filename: "picker".to_string(), 475 + source: String::new(), 476 + debug_input: false, 477 + watch: false, 478 + filepath: None, 479 + last_file_state: None, 480 + }, 481 + ); 482 + 483 + assert!(app.open_fuzzy_file_picker(root.clone())); 484 + app.push_file_picker_query('c'); 485 + app.push_file_picker_query('a'); 486 + 487 + let labels: Vec<_> = app 488 + .file_picker_filtered_indices() 489 + .iter() 490 + .map(|idx| app.file_picker_entries()[*idx].label()) 491 + .collect(); 492 + let showcase_idx = labels 493 + .iter() 494 + .position(|label| *label == ".notes/tests/themes-showcase.md") 495 + .unwrap(); 496 + let chatgpt_idx = labels 497 + .iter() 498 + .position(|label| *label == ".notes/todo/review-chatgpt.md") 499 + .unwrap(); 500 + assert!(showcase_idx < chatgpt_idx); 501 + 502 + let _ = fs::remove_dir_all(root); 503 + } 504 + 505 + #[test] 506 + fn fuzzy_file_picker_prefers_filename_prefix_matches() { 507 + let unique = SystemTime::now() 508 + .duration_since(UNIX_EPOCH) 509 + .unwrap() 510 + .as_nanos(); 511 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-prefix-{unique}")); 512 + let _ = fs::remove_dir_all(&root); 513 + fs::create_dir_all(&root).unwrap(); 514 + fs::write(root.join("todo-case.md"), "# Todo\n").unwrap(); 515 + fs::write(root.join("case-study.md"), "# Case\n").unwrap(); 516 + 517 + let mut app = App::new_with_source( 518 + Vec::new(), 519 + Vec::new(), 520 + AppConfig { 521 + filename: "picker".to_string(), 522 + source: String::new(), 523 + debug_input: false, 524 + watch: false, 525 + filepath: None, 526 + last_file_state: None, 527 + }, 528 + ); 529 + 530 + assert!(app.open_fuzzy_file_picker(root.clone())); 531 + app.push_file_picker_query('c'); 532 + app.push_file_picker_query('a'); 533 + 534 + let labels: Vec<_> = app 535 + .file_picker_filtered_indices() 536 + .iter() 537 + .map(|idx| app.file_picker_entries()[*idx].label()) 538 + .collect(); 539 + assert_eq!(labels, vec!["case-study.md", "todo-case.md"]); 540 + 541 + let _ = fs::remove_dir_all(root); 542 + } 543 + 544 + #[test] 545 + fn fuzzy_file_picker_prefers_token_boundary_matches() { 546 + let unique = SystemTime::now() 547 + .duration_since(UNIX_EPOCH) 548 + .unwrap() 549 + .as_nanos(); 550 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-boundary-{unique}")); 551 + let _ = fs::remove_dir_all(&root); 552 + fs::create_dir_all(&root).unwrap(); 553 + fs::write(root.join("alpha-case.md"), "# Boundary\n").unwrap(); 554 + fs::write(root.join("alphacase.md"), "# Plain\n").unwrap(); 555 + 556 + let mut app = App::new_with_source( 557 + Vec::new(), 558 + Vec::new(), 559 + AppConfig { 560 + filename: "picker".to_string(), 561 + source: String::new(), 562 + debug_input: false, 563 + watch: false, 564 + filepath: None, 565 + last_file_state: None, 566 + }, 567 + ); 568 + 569 + assert!(app.open_fuzzy_file_picker(root.clone())); 570 + app.push_file_picker_query('c'); 571 + app.push_file_picker_query('a'); 572 + 573 + let labels: Vec<_> = app 574 + .file_picker_filtered_indices() 575 + .iter() 576 + .map(|idx| app.file_picker_entries()[*idx].label()) 577 + .collect(); 578 + assert_eq!(labels, vec!["alpha-case.md", "alphacase.md"]); 579 + 580 + let _ = fs::remove_dir_all(root); 581 + } 582 + 583 + #[test] 584 + fn fuzzy_file_picker_prefers_shallower_paths_on_equal_scores() { 585 + let unique = SystemTime::now() 586 + .duration_since(UNIX_EPOCH) 587 + .unwrap() 588 + .as_nanos(); 589 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-depth-{unique}")); 590 + let _ = fs::remove_dir_all(&root); 591 + fs::create_dir_all(root.join("nested/deeper")).unwrap(); 592 + fs::write(root.join("case.md"), "# Root\n").unwrap(); 593 + fs::write(root.join("nested/deeper/case.md"), "# Nested\n").unwrap(); 594 + 595 + let mut app = App::new_with_source( 596 + Vec::new(), 597 + Vec::new(), 598 + AppConfig { 599 + filename: "picker".to_string(), 600 + source: String::new(), 601 + debug_input: false, 602 + watch: false, 603 + filepath: None, 604 + last_file_state: None, 605 + }, 606 + ); 607 + 608 + assert!(app.open_fuzzy_file_picker(root.clone())); 609 + app.push_file_picker_query('c'); 610 + app.push_file_picker_query('a'); 611 + app.push_file_picker_query('s'); 612 + app.push_file_picker_query('e'); 613 + 614 + let labels: Vec<_> = app 615 + .file_picker_filtered_indices() 616 + .iter() 617 + .map(|idx| app.file_picker_entries()[*idx].label()) 618 + .collect(); 619 + assert_eq!(labels, vec!["case.md", "nested/deeper/case.md"]); 620 + 621 + let _ = fs::remove_dir_all(root); 622 + } 623 + 624 + #[test] 625 + fn fuzzy_file_picker_allows_q_in_query() { 626 + let unique = SystemTime::now() 627 + .duration_since(UNIX_EPOCH) 628 + .unwrap() 629 + .as_nanos(); 630 + let root = std::env::temp_dir().join(format!("leaf-fuzzy-picker-q-{unique}")); 631 + let _ = fs::remove_dir_all(&root); 632 + fs::create_dir_all(&root).unwrap(); 633 + fs::write(root.join("query.md"), "# Query\n").unwrap(); 634 + 635 + let mut app = App::new_with_source( 636 + Vec::new(), 637 + Vec::new(), 638 + AppConfig { 639 + filename: "picker".to_string(), 640 + source: String::new(), 641 + debug_input: false, 642 + watch: false, 643 + filepath: None, 644 + last_file_state: None, 645 + }, 646 + ); 647 + 648 + assert!(app.open_fuzzy_file_picker(root.clone())); 649 + app.push_file_picker_query('q'); 650 + assert_eq!(app.file_picker_query(), "q"); 651 + 652 + let labels: Vec<_> = app 653 + .file_picker_filtered_indices() 654 + .iter() 655 + .map(|idx| app.file_picker_entries()[*idx].label()) 656 + .collect(); 657 + assert_eq!(labels, vec!["query.md"]); 658 + 659 + let _ = fs::remove_dir_all(root); 660 + } 661 + 662 + #[test] 663 + fn fuzzy_file_picker_skips_ignored_technical_directories() { 664 + let root = unique_temp_dir("leaf-fuzzy-picker-ignore"); 665 + let _ = fs::remove_dir_all(&root); 666 + fs::create_dir_all(root.join(".git")).unwrap(); 667 + fs::create_dir_all(root.join("target")).unwrap(); 668 + fs::create_dir_all(root.join("vendor")).unwrap(); 669 + fs::create_dir_all(root.join("var")).unwrap(); 670 + fs::create_dir_all(root.join(".notes")).unwrap(); 671 + fs::write(root.join(".git/ignored.md"), "# Ignored\n").unwrap(); 672 + fs::write(root.join("target/ignored.md"), "# Ignored\n").unwrap(); 673 + fs::write(root.join("vendor/ignored.md"), "# Ignored\n").unwrap(); 674 + fs::write(root.join("var/ignored.md"), "# Ignored\n").unwrap(); 675 + fs::write(root.join(".notes/kept.md"), "# Kept\n").unwrap(); 676 + 677 + let mut app = App::new_with_source( 678 + Vec::new(), 679 + Vec::new(), 680 + AppConfig { 681 + filename: "picker".to_string(), 682 + source: String::new(), 683 + debug_input: false, 684 + watch: false, 685 + filepath: None, 686 + last_file_state: None, 687 + }, 688 + ); 689 + 690 + assert!(app.open_fuzzy_file_picker(root.clone())); 691 + 692 + let labels: Vec<_> = app 693 + .file_picker_filtered_indices() 694 + .iter() 695 + .map(|idx| app.file_picker_entries()[*idx].label()) 696 + .collect(); 697 + assert_eq!(labels, vec![".notes/kept.md"]); 698 + assert_eq!(app.file_picker_truncation(), None); 699 + 700 + let _ = fs::remove_dir_all(root); 701 + } 702 + 703 + #[test] 704 + fn fuzzy_file_picker_reports_directory_limit_truncation() { 705 + let root = unique_temp_dir("leaf-fuzzy-picker-dir-limit"); 706 + let _ = fs::remove_dir_all(&root); 707 + for idx in 0..5_050usize { 708 + let dir = root.join(format!("nested-{idx:04}")); 709 + fs::create_dir_all(&dir).unwrap(); 710 + fs::write(dir.join(format!("file-{idx:04}.md")), "# File\n").unwrap(); 711 + } 712 + 713 + let mut app = App::new_with_source( 714 + Vec::new(), 715 + Vec::new(), 716 + AppConfig { 717 + filename: "picker".to_string(), 718 + source: String::new(), 719 + debug_input: false, 720 + watch: false, 721 + filepath: None, 722 + last_file_state: None, 723 + }, 724 + ); 725 + 726 + assert!(app.open_fuzzy_file_picker(root.clone())); 727 + assert_eq!( 728 + app.file_picker_truncation(), 729 + Some(crate::app::PickerIndexTruncation::Directory) 730 + ); 731 + assert!(!app.file_picker_entries().is_empty()); 732 + 733 + let _ = fs::remove_dir_all(root); 734 + } 735 + 736 + #[test] 737 + fn fuzzy_file_picker_reports_file_limit_truncation() { 738 + let root = unique_temp_dir("leaf-fuzzy-picker-file-limit"); 739 + let _ = fs::remove_dir_all(&root); 740 + fs::create_dir_all(&root).unwrap(); 741 + for idx in 0..10_050usize { 742 + fs::write(root.join(format!("file-{idx:05}.md")), "# File\n").unwrap(); 743 + } 744 + 745 + let mut app = App::new_with_source( 746 + Vec::new(), 747 + Vec::new(), 748 + AppConfig { 749 + filename: "picker".to_string(), 750 + source: String::new(), 751 + debug_input: false, 752 + watch: false, 753 + filepath: None, 754 + last_file_state: None, 755 + }, 756 + ); 757 + 758 + assert!(app.open_fuzzy_file_picker(root.clone())); 759 + assert_eq!( 760 + app.file_picker_truncation(), 761 + Some(crate::app::PickerIndexTruncation::File) 762 + ); 763 + assert_eq!(app.file_picker_entries().len(), 10_000); 764 + 765 + let _ = fs::remove_dir_all(root); 766 + }
+1
src/tests/mod.rs
··· 12 12 }; 13 13 14 14 mod app; 15 + mod file_picker; 15 16 mod markdown; 16 17 mod render; 17 18 mod theme;