@@ -3,6 +3,8 @@ module unified_search_module |
| 3 | 3 | use terminal_io_module |
| 4 | 4 | use editor_state_module, only: editor_state_t, cursor_t |
| 5 | 5 | use text_buffer_module |
| 6 | + use regex_module |
| 7 | + use renderer_module, only: render_screen |
| 6 | 8 | implicit none |
| 7 | 9 | private |
| 8 | 10 | |
@@ -23,6 +25,12 @@ module unified_search_module |
| 23 | 25 | integer :: total_matches = 0 |
| 24 | 26 | integer :: current_match_index = 0 |
| 25 | 27 | |
| 28 | + ! Compiled regex ID (when regex mode is active) |
| 29 | + integer :: compiled_regex_id = -1 |
| 30 | + |
| 31 | + ! Length of last match (needed for regex where match length != pattern length) |
| 32 | + integer :: last_match_length = 0 |
| 33 | + |
| 26 | 34 | ! Active search mode - persists after first search |
| 27 | 35 | logical :: search_mode_active = .false. |
| 28 | 36 | |
@@ -300,6 +308,20 @@ contains |
| 300 | 308 | logical :: found |
| 301 | 309 | integer :: found_line, found_col |
| 302 | 310 | |
| 311 | + ! Compile regex if in regex mode |
| 312 | + if (use_regex) then |
| 313 | + ! Free old regex if any |
| 314 | + if (compiled_regex_id >= 0) then |
| 315 | + call regex_free(compiled_regex_id) |
| 316 | + end if |
| 317 | + ! Compile new pattern |
| 318 | + compiled_regex_id = regex_compile(pattern, case_sensitive) |
| 319 | + if (compiled_regex_id < 0) then |
| 320 | + ! Regex compilation failed - could show error, for now just skip |
| 321 | + return |
| 322 | + end if |
| 323 | + end if |
| 324 | + |
| 303 | 325 | call find_next_match(buffer, pattern, & |
| 304 | 326 | editor%cursors(editor%active_cursor)%line, & |
| 305 | 327 | editor%cursors(editor%active_cursor)%column, & |
@@ -311,15 +333,22 @@ contains |
| 311 | 333 | editor%cursors(editor%active_cursor)%desired_column = found_col |
| 312 | 334 | |
| 313 | 335 | ! Create selection |
| 336 | + ! For regex, use the match length from last search |
| 337 | + ! For normal search, use pattern length |
| 314 | 338 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 315 | 339 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 316 | 340 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 317 | | - editor%cursors(editor%active_cursor)%column = found_col + len(pattern) |
| 341 | + if (use_regex .and. last_match_length > 0) then |
| 342 | + editor%cursors(editor%active_cursor)%column = found_col + last_match_length |
| 343 | + else |
| 344 | + editor%cursors(editor%active_cursor)%column = found_col + len(pattern) |
| 345 | + end if |
| 318 | 346 | |
| 319 | 347 | last_search_line = found_line |
| 320 | 348 | last_search_col = found_col |
| 321 | 349 | |
| 322 | 350 | call center_viewport_on_cursor(editor) |
| 351 | + call render_screen(buffer, editor) |
| 323 | 352 | end if |
| 324 | 353 | end subroutine perform_search |
| 325 | 354 | |
@@ -351,12 +380,19 @@ contains |
| 351 | 380 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 352 | 381 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 353 | 382 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 354 | | - editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern) |
| 383 | + |
| 384 | + ! Use last_match_length for regex (which was set by find_next_match) |
| 385 | + if (use_regex .and. last_match_length > 0) then |
| 386 | + editor%cursors(editor%active_cursor)%column = found_col + last_match_length |
| 387 | + else |
| 388 | + editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern) |
| 389 | + end if |
| 355 | 390 | |
| 356 | 391 | last_search_line = found_line |
| 357 | 392 | last_search_col = found_col |
| 358 | 393 | |
| 359 | 394 | call center_viewport_on_cursor(editor) |
| 395 | + call render_screen(buffer, editor) |
| 360 | 396 | end if |
| 361 | 397 | end subroutine search_forward |
| 362 | 398 | |
@@ -370,11 +406,17 @@ contains |
| 370 | 406 | ! If cursor has selection, replace it |
| 371 | 407 | if (editor%cursors(editor%active_cursor)%has_selection) then |
| 372 | 408 | call perform_replacement(buffer, editor%cursors(editor%active_cursor), & |
| 373 | | - current_search_pattern, current_replace_text) |
| 374 | | - end if |
| 409 | + current_replace_text, last_match_length) |
| 410 | + |
| 411 | + ! Clear selection after replacement |
| 412 | + editor%cursors(editor%active_cursor)%has_selection = .false. |
| 375 | 413 | |
| 376 | | - ! Find next match |
| 377 | | - call search_forward(editor, buffer) |
| 414 | + ! Re-count matches after replacement |
| 415 | + call count_all_matches(buffer, current_search_pattern) |
| 416 | + |
| 417 | + ! Render to show the replacement (without selection) |
| 418 | + call render_screen(buffer, editor) |
| 419 | + end if |
| 378 | 420 | end subroutine replace_current_and_advance |
| 379 | 421 | |
| 380 | 422 | subroutine replace_all_matches(editor, buffer) |
@@ -407,10 +449,10 @@ contains |
| 407 | 449 | temp_cursor%has_selection = .true. |
| 408 | 450 | temp_cursor%selection_start_line = found_line |
| 409 | 451 | temp_cursor%selection_start_col = found_col |
| 410 | | - temp_cursor%column = found_col + len(current_search_pattern) |
| 452 | + temp_cursor%column = found_col + last_match_length |
| 411 | 453 | |
| 412 | 454 | ! Replace |
| 413 | | - call perform_replacement(buffer, temp_cursor, current_search_pattern, current_replace_text) |
| 455 | + call perform_replacement(buffer, temp_cursor, current_replace_text, last_match_length) |
| 414 | 456 | |
| 415 | 457 | replace_count = replace_count + 1 |
| 416 | 458 | |
@@ -422,10 +464,11 @@ contains |
| 422 | 464 | editor%cursors(editor%active_cursor) = temp_cursor |
| 423 | 465 | end subroutine replace_all_matches |
| 424 | 466 | |
| 425 | | - subroutine perform_replacement(buffer, cursor, find_pattern, replace_text) |
| 467 | + subroutine perform_replacement(buffer, cursor, replace_text, match_len) |
| 426 | 468 | type(buffer_t), intent(inout) :: buffer |
| 427 | 469 | type(cursor_t), intent(inout) :: cursor |
| 428 | | - character(len=*), intent(in) :: find_pattern, replace_text |
| 470 | + character(len=*), intent(in) :: replace_text |
| 471 | + integer, intent(in) :: match_len |
| 429 | 472 | character(len=:), allocatable :: line, new_line |
| 430 | 473 | integer :: col, i |
| 431 | 474 | |
@@ -434,7 +477,7 @@ contains |
| 434 | 477 | |
| 435 | 478 | ! Build new line with replacement |
| 436 | 479 | col = cursor%selection_start_col |
| 437 | | - allocate(character(len=len(line) - len(find_pattern) + len(replace_text)) :: new_line) |
| 480 | + allocate(character(len=len(line) - match_len + len(replace_text)) :: new_line) |
| 438 | 481 | |
| 439 | 482 | ! Copy part before match |
| 440 | 483 | if (col > 1) then |
@@ -447,8 +490,8 @@ contains |
| 447 | 490 | end if |
| 448 | 491 | |
| 449 | 492 | ! Copy part after match |
| 450 | | - if (col + len(find_pattern) <= len(line)) then |
| 451 | | - new_line(col+len(replace_text):) = line(col+len(find_pattern):) |
| 493 | + if (col + match_len <= len(line)) then |
| 494 | + new_line(col+len(replace_text):) = line(col+match_len:) |
| 452 | 495 | end if |
| 453 | 496 | |
| 454 | 497 | ! Delete old line content |
@@ -545,6 +588,13 @@ contains |
| 545 | 588 | search_mode_active = .false. |
| 546 | 589 | last_search_line = 1 |
| 547 | 590 | last_search_col = 1 |
| 591 | + |
| 592 | + ! Free compiled regex if any |
| 593 | + if (compiled_regex_id >= 0) then |
| 594 | + call regex_free(compiled_regex_id) |
| 595 | + compiled_regex_id = -1 |
| 596 | + end if |
| 597 | + last_match_length = 0 |
| 548 | 598 | end subroutine clear_search_pattern |
| 549 | 599 | |
| 550 | 600 | ! Search helper functions |
@@ -556,11 +606,13 @@ contains |
| 556 | 606 | integer, intent(out) :: found_line, found_col |
| 557 | 607 | character(len=:), allocatable :: line |
| 558 | 608 | integer :: line_count, current_line, pos, search_col |
| 609 | + integer :: match_len |
| 559 | 610 | |
| 560 | 611 | found = .false. |
| 561 | 612 | found_line = 0 |
| 562 | 613 | found_col = 0 |
| 563 | 614 | line_count = buffer_get_line_count(buffer) |
| 615 | + last_match_length = 0 |
| 564 | 616 | |
| 565 | 617 | ! Search from current position to end |
| 566 | 618 | do current_line = start_line, line_count |
@@ -571,17 +623,23 @@ contains |
| 571 | 623 | search_col = 1 |
| 572 | 624 | end if |
| 573 | 625 | if (search_col <= len(line)) then |
| 574 | | - call find_pattern_in_line(line(search_col:), pattern, pos) |
| 626 | + if (use_regex) then |
| 627 | + call find_regex_in_line(line(search_col:), compiled_regex_id, pos, match_len) |
| 628 | + else |
| 629 | + call find_pattern_in_line(line(search_col:), pattern, pos) |
| 630 | + match_len = len(pattern) |
| 631 | + end if |
| 575 | 632 | if (pos > 0) then |
| 576 | 633 | found_col = search_col + pos - 1 |
| 577 | 634 | if (whole_word) then |
| 578 | | - if (.not. is_whole_word_match(line, found_col, len(pattern))) then |
| 635 | + if (.not. is_whole_word_match(line, found_col, match_len)) then |
| 579 | 636 | if (allocated(line)) deallocate(line) |
| 580 | 637 | cycle |
| 581 | 638 | end if |
| 582 | 639 | end if |
| 583 | 640 | found = .true. |
| 584 | 641 | found_line = current_line |
| 642 | + last_match_length = match_len |
| 585 | 643 | if (allocated(line)) deallocate(line) |
| 586 | 644 | call update_match_index(buffer, pattern, found_line, found_col) |
| 587 | 645 | return |
@@ -595,16 +653,27 @@ contains |
| 595 | 653 | line = buffer_get_line(buffer, current_line) |
| 596 | 654 | if (current_line == start_line) then |
| 597 | 655 | if (start_col > 1) then |
| 598 | | - call find_pattern_in_line(line(1:start_col-1), pattern, pos) |
| 656 | + if (use_regex) then |
| 657 | + call find_regex_in_line(line(1:start_col-1), compiled_regex_id, pos, match_len) |
| 658 | + else |
| 659 | + call find_pattern_in_line(line(1:start_col-1), pattern, pos) |
| 660 | + match_len = len(pattern) |
| 661 | + end if |
| 599 | 662 | else |
| 600 | 663 | pos = 0 |
| 664 | + match_len = 0 |
| 601 | 665 | end if |
| 602 | 666 | else |
| 603 | | - call find_pattern_in_line(line, pattern, pos) |
| 667 | + if (use_regex) then |
| 668 | + call find_regex_in_line(line, compiled_regex_id, pos, match_len) |
| 669 | + else |
| 670 | + call find_pattern_in_line(line, pattern, pos) |
| 671 | + match_len = len(pattern) |
| 672 | + end if |
| 604 | 673 | end if |
| 605 | 674 | if (pos > 0) then |
| 606 | 675 | if (whole_word) then |
| 607 | | - if (.not. is_whole_word_match(line, pos, len(pattern))) then |
| 676 | + if (.not. is_whole_word_match(line, pos, match_len)) then |
| 608 | 677 | if (allocated(line)) deallocate(line) |
| 609 | 678 | cycle |
| 610 | 679 | end if |
@@ -612,6 +681,7 @@ contains |
| 612 | 681 | found = .true. |
| 613 | 682 | found_line = current_line |
| 614 | 683 | found_col = pos |
| 684 | + last_match_length = match_len |
| 615 | 685 | if (allocated(line)) deallocate(line) |
| 616 | 686 | call update_match_index(buffer, pattern, found_line, found_col) |
| 617 | 687 | return |
@@ -628,11 +698,13 @@ contains |
| 628 | 698 | integer, intent(out) :: found_line, found_col |
| 629 | 699 | character(len=:), allocatable :: line |
| 630 | 700 | integer :: line_count, current_line, pos, last_pos, check_col |
| 701 | + integer :: match_len, last_match_len |
| 631 | 702 | |
| 632 | 703 | found = .false. |
| 633 | 704 | found_line = 0 |
| 634 | 705 | found_col = 0 |
| 635 | 706 | line_count = buffer_get_line_count(buffer) |
| 707 | + last_match_length = 0 |
| 636 | 708 | |
| 637 | 709 | ! Search backward from current position |
| 638 | 710 | do current_line = start_line, 1, -1 |
@@ -644,16 +716,24 @@ contains |
| 644 | 716 | end if |
| 645 | 717 | |
| 646 | 718 | last_pos = 0 |
| 719 | + last_match_len = 0 |
| 647 | 720 | pos = 1 |
| 648 | | - do while (pos <= check_col - len(pattern) + 1) |
| 649 | | - call find_pattern_in_line(line(pos:), pattern, found_col) |
| 721 | + do while (pos <= check_col) |
| 722 | + if (use_regex) then |
| 723 | + call find_regex_in_line(line(pos:), compiled_regex_id, found_col, match_len) |
| 724 | + else |
| 725 | + call find_pattern_in_line(line(pos:), pattern, found_col) |
| 726 | + match_len = len(pattern) |
| 727 | + end if |
| 650 | 728 | if (found_col > 0 .and. pos + found_col - 1 <= check_col) then |
| 651 | 729 | if (whole_word) then |
| 652 | | - if (is_whole_word_match(line, pos + found_col - 1, len(pattern))) then |
| 730 | + if (is_whole_word_match(line, pos + found_col - 1, match_len)) then |
| 653 | 731 | last_pos = pos + found_col - 1 |
| 732 | + last_match_len = match_len |
| 654 | 733 | end if |
| 655 | 734 | else |
| 656 | 735 | last_pos = pos + found_col - 1 |
| 736 | + last_match_len = match_len |
| 657 | 737 | end if |
| 658 | 738 | pos = pos + found_col |
| 659 | 739 | else |
@@ -665,6 +745,7 @@ contains |
| 665 | 745 | found = .true. |
| 666 | 746 | found_line = current_line |
| 667 | 747 | found_col = last_pos |
| 748 | + last_match_length = last_match_len |
| 668 | 749 | if (allocated(line)) deallocate(line) |
| 669 | 750 | call update_match_index(buffer, pattern, found_line, found_col) |
| 670 | 751 | return |
@@ -709,6 +790,32 @@ contains |
| 709 | 790 | end if |
| 710 | 791 | end subroutine find_pattern_in_line |
| 711 | 792 | |
| 793 | + ! Find pattern using regex in a line |
| 794 | + ! Returns position where match starts (1-based), or 0 if not found |
| 795 | + ! Also returns match_len (length of what was matched) |
| 796 | + subroutine find_regex_in_line(line, regex_id, pos, match_len) |
| 797 | + character(len=*), intent(in) :: line |
| 798 | + integer, intent(in) :: regex_id |
| 799 | + integer, intent(out) :: pos, match_len |
| 800 | + logical :: found |
| 801 | + integer :: start_pos, len_matched |
| 802 | + |
| 803 | + if (regex_id < 0) then |
| 804 | + pos = 0 |
| 805 | + match_len = 0 |
| 806 | + return |
| 807 | + end if |
| 808 | + |
| 809 | + found = regex_match(regex_id, line, start_pos, len_matched) |
| 810 | + if (found) then |
| 811 | + pos = start_pos |
| 812 | + match_len = len_matched |
| 813 | + else |
| 814 | + pos = 0 |
| 815 | + match_len = 0 |
| 816 | + end if |
| 817 | + end subroutine find_regex_in_line |
| 818 | + |
| 712 | 819 | logical function is_whole_word_match(line, start_pos, pattern_len) |
| 713 | 820 | character(len=*), intent(in) :: line |
| 714 | 821 | integer, intent(in) :: start_pos, pattern_len |
@@ -745,6 +852,7 @@ contains |
| 745 | 852 | character(len=*), intent(in) :: pattern |
| 746 | 853 | character(len=:), allocatable :: line |
| 747 | 854 | integer :: line_count, current_line, pos, col, match_count |
| 855 | + integer :: match_len |
| 748 | 856 | |
| 749 | 857 | match_count = 0 |
| 750 | 858 | line_count = buffer_get_line_count(buffer) |
@@ -753,11 +861,16 @@ contains |
| 753 | 861 | line = buffer_get_line(buffer, current_line) |
| 754 | 862 | col = 1 |
| 755 | 863 | do while (col <= len(line)) |
| 756 | | - call find_pattern_in_line(line(col:), pattern, pos) |
| 864 | + if (use_regex) then |
| 865 | + call find_regex_in_line(line(col:), compiled_regex_id, pos, match_len) |
| 866 | + else |
| 867 | + call find_pattern_in_line(line(col:), pattern, pos) |
| 868 | + match_len = len(pattern) |
| 869 | + end if |
| 757 | 870 | if (pos > 0) then |
| 758 | 871 | pos = col + pos - 1 |
| 759 | 872 | if (whole_word) then |
| 760 | | - if (is_whole_word_match(line, pos, len(pattern))) then |
| 873 | + if (is_whole_word_match(line, pos, match_len)) then |
| 761 | 874 | match_count = match_count + 1 |
| 762 | 875 | end if |
| 763 | 876 | else |
@@ -781,6 +894,7 @@ contains |
| 781 | 894 | integer, intent(in) :: match_line, match_col |
| 782 | 895 | character(len=:), allocatable :: line |
| 783 | 896 | integer :: line_count, current_line, pos, col, match_count |
| 897 | + integer :: match_len |
| 784 | 898 | |
| 785 | 899 | match_count = 0 |
| 786 | 900 | line_count = buffer_get_line_count(buffer) |
@@ -789,11 +903,16 @@ contains |
| 789 | 903 | line = buffer_get_line(buffer, current_line) |
| 790 | 904 | col = 1 |
| 791 | 905 | do while (col <= len(line)) |
| 792 | | - call find_pattern_in_line(line(col:), pattern, pos) |
| 906 | + if (use_regex) then |
| 907 | + call find_regex_in_line(line(col:), compiled_regex_id, pos, match_len) |
| 908 | + else |
| 909 | + call find_pattern_in_line(line(col:), pattern, pos) |
| 910 | + match_len = len(pattern) |
| 911 | + end if |
| 793 | 912 | if (pos > 0) then |
| 794 | 913 | pos = col + pos - 1 |
| 795 | 914 | if (whole_word) then |
| 796 | | - if (is_whole_word_match(line, pos, len(pattern))) then |
| 915 | + if (is_whole_word_match(line, pos, match_len)) then |
| 797 | 916 | match_count = match_count + 1 |
| 798 | 917 | if (current_line == match_line .and. pos == match_col) then |
| 799 | 918 | current_match_index = match_count |