| 1 | module search_prompt_module |
| 2 | use iso_fortran_env, only: input_unit, output_unit |
| 3 | use terminal_io_module |
| 4 | use editor_state_module, only: editor_state_t, cursor_t |
| 5 | use text_buffer_module |
| 6 | implicit none |
| 7 | private |
| 8 | |
| 9 | public :: show_search_prompt, search_forward, search_backward |
| 10 | public :: current_search_pattern, clear_search_pattern |
| 11 | public :: find_next_match, find_prev_match, center_viewport_on_cursor |
| 12 | |
| 13 | ! Module variables for search state |
| 14 | character(len=:), allocatable :: current_search_pattern |
| 15 | integer :: last_search_line = 1 |
| 16 | integer :: last_search_col = 1 |
| 17 | |
| 18 | ! Search options |
| 19 | logical :: case_sensitive = .false. |
| 20 | logical :: whole_word = .false. |
| 21 | integer :: total_matches = 0 |
| 22 | integer :: current_match_index = 0 |
| 23 | |
| 24 | contains |
| 25 | |
| 26 | subroutine show_search_prompt(editor, buffer) |
| 27 | type(editor_state_t), intent(inout) :: editor |
| 28 | type(buffer_t), intent(inout) :: buffer |
| 29 | character(len=256) :: input_buffer |
| 30 | character(len=128) :: prompt |
| 31 | integer :: input_pos, ch |
| 32 | logical :: found |
| 33 | integer :: found_line, found_col |
| 34 | logical :: in_alt_sequence |
| 35 | |
| 36 | ! Initialize |
| 37 | input_buffer = '' |
| 38 | input_pos = 0 |
| 39 | in_alt_sequence = .false. |
| 40 | |
| 41 | ! Build prompt with options |
| 42 | call build_search_prompt(prompt) |
| 43 | |
| 44 | ! Display prompt at bottom of screen |
| 45 | call terminal_move_cursor(editor%screen_rows, 1) |
| 46 | call terminal_write(prompt) |
| 47 | call terminal_show_cursor() |
| 48 | |
| 49 | ! Input loop |
| 50 | do |
| 51 | ch = terminal_read_char() |
| 52 | |
| 53 | if (ch == -1) then |
| 54 | ! No input, continue |
| 55 | cycle |
| 56 | else if (ch == 27) then ! ESC |
| 57 | ! Check if this is an Alt sequence or standalone ESC |
| 58 | in_alt_sequence = .true. |
| 59 | ch = terminal_read_char() |
| 60 | |
| 61 | if (ch == -1 .or. ch == 27) then |
| 62 | ! Standalone ESC - cancel search |
| 63 | if (allocated(current_search_pattern)) then |
| 64 | deallocate(current_search_pattern) |
| 65 | end if |
| 66 | exit |
| 67 | else if (ch == iachar('c') .or. ch == iachar('C')) then |
| 68 | ! Alt+C - toggle case sensitive |
| 69 | case_sensitive = .not. case_sensitive |
| 70 | call build_search_prompt(prompt) |
| 71 | call terminal_move_cursor(editor%screen_rows, 1) |
| 72 | ! Clear the entire line first to avoid duplicate text |
| 73 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 74 | call terminal_move_cursor(editor%screen_rows, 1) |
| 75 | call terminal_write(prompt // input_buffer(1:input_pos)) |
| 76 | call terminal_move_cursor(editor%screen_rows, len_trim(prompt) + input_pos + 1) |
| 77 | in_alt_sequence = .false. |
| 78 | else if (ch == iachar('w') .or. ch == iachar('W')) then |
| 79 | ! Alt+W - toggle whole word |
| 80 | whole_word = .not. whole_word |
| 81 | call build_search_prompt(prompt) |
| 82 | call terminal_move_cursor(editor%screen_rows, 1) |
| 83 | ! Clear the entire line first to avoid duplicate text |
| 84 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 85 | call terminal_move_cursor(editor%screen_rows, 1) |
| 86 | call terminal_write(prompt // input_buffer(1:input_pos)) |
| 87 | call terminal_move_cursor(editor%screen_rows, len_trim(prompt) + input_pos + 1) |
| 88 | in_alt_sequence = .false. |
| 89 | else |
| 90 | ! Unknown Alt sequence, ignore |
| 91 | in_alt_sequence = .false. |
| 92 | end if |
| 93 | else if (ch == 10 .or. ch == 13) then ! Enter - accept |
| 94 | if (input_pos > 0) then |
| 95 | ! Save search pattern |
| 96 | if (allocated(current_search_pattern)) then |
| 97 | deallocate(current_search_pattern) |
| 98 | end if |
| 99 | allocate(character(len=input_pos) :: current_search_pattern) |
| 100 | current_search_pattern = input_buffer(1:input_pos) |
| 101 | |
| 102 | ! Count all matches first |
| 103 | call count_all_matches(buffer, current_search_pattern) |
| 104 | |
| 105 | ! Search from current position |
| 106 | call find_next_match(buffer, current_search_pattern, & |
| 107 | editor%cursors(editor%active_cursor)%line, & |
| 108 | editor%cursors(editor%active_cursor)%column, & |
| 109 | found, found_line, found_col) |
| 110 | |
| 111 | if (found) then |
| 112 | ! Move cursor to match and select it |
| 113 | editor%cursors(editor%active_cursor)%line = found_line |
| 114 | editor%cursors(editor%active_cursor)%column = found_col |
| 115 | editor%cursors(editor%active_cursor)%desired_column = found_col |
| 116 | |
| 117 | ! Create selection for the match |
| 118 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 119 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 120 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 121 | editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern) |
| 122 | |
| 123 | ! Update last search position |
| 124 | last_search_line = found_line |
| 125 | last_search_col = found_col |
| 126 | |
| 127 | ! Center viewport on match |
| 128 | call center_viewport_on_cursor(editor) |
| 129 | else |
| 130 | ! Show "not found" message briefly |
| 131 | call terminal_move_cursor(editor%screen_rows, 1) |
| 132 | call terminal_write('Pattern not found: ' // current_search_pattern) |
| 133 | call flush(output_unit) |
| 134 | ! Note: In a real implementation, we'd want a better way to show this |
| 135 | end if |
| 136 | end if |
| 137 | exit |
| 138 | else if (ch == 127 .or. ch == 8) then ! Backspace |
| 139 | if (input_pos > 0) then |
| 140 | input_pos = input_pos - 1 |
| 141 | ! Redraw prompt and input |
| 142 | call terminal_move_cursor(editor%screen_rows, 1) |
| 143 | call terminal_write(prompt // input_buffer(1:input_pos) // ' ') |
| 144 | call terminal_move_cursor(editor%screen_rows, len(prompt) + input_pos + 1) |
| 145 | |
| 146 | ! Incremental search with reduced pattern |
| 147 | if (input_pos > 0) then |
| 148 | call perform_incremental_search(editor, buffer, input_buffer(1:input_pos)) |
| 149 | end if |
| 150 | end if |
| 151 | else if (ch >= 32 .and. ch <= 126) then ! Printable characters |
| 152 | if (input_pos < 256) then |
| 153 | input_pos = input_pos + 1 |
| 154 | input_buffer(input_pos:input_pos) = char(ch) |
| 155 | call terminal_write(char(ch)) |
| 156 | |
| 157 | ! Incremental search - search as user types |
| 158 | if (input_pos > 0) then |
| 159 | call perform_incremental_search(editor, buffer, input_buffer(1:input_pos)) |
| 160 | end if |
| 161 | end if |
| 162 | end if |
| 163 | end do |
| 164 | |
| 165 | ! Clean up - hide cursor and clear prompt line |
| 166 | call terminal_hide_cursor() |
| 167 | call terminal_move_cursor(editor%screen_rows, 1) |
| 168 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 169 | end subroutine show_search_prompt |
| 170 | |
| 171 | subroutine search_forward(editor, buffer) |
| 172 | type(editor_state_t), intent(inout) :: editor |
| 173 | type(buffer_t), intent(inout) :: buffer |
| 174 | logical :: found |
| 175 | integer :: found_line, found_col |
| 176 | integer :: start_line, start_col |
| 177 | |
| 178 | if (.not. allocated(current_search_pattern)) return |
| 179 | |
| 180 | ! Re-count matches in case search options changed |
| 181 | call count_all_matches(buffer, current_search_pattern) |
| 182 | |
| 183 | ! Search from cursor position |
| 184 | start_line = editor%cursors(editor%active_cursor)%line |
| 185 | start_col = editor%cursors(editor%active_cursor)%column |
| 186 | |
| 187 | call find_next_match(buffer, current_search_pattern, & |
| 188 | start_line, start_col, & |
| 189 | found, found_line, found_col) |
| 190 | |
| 191 | if (found) then |
| 192 | ! Move cursor to match and select it |
| 193 | editor%cursors(editor%active_cursor)%line = found_line |
| 194 | editor%cursors(editor%active_cursor)%column = found_col |
| 195 | editor%cursors(editor%active_cursor)%desired_column = found_col |
| 196 | |
| 197 | ! Create selection for the match |
| 198 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 199 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 200 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 201 | editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern) |
| 202 | |
| 203 | ! Update last search position |
| 204 | last_search_line = found_line |
| 205 | last_search_col = found_col |
| 206 | |
| 207 | ! Update viewport |
| 208 | call center_viewport_on_cursor(editor) |
| 209 | end if |
| 210 | end subroutine search_forward |
| 211 | |
| 212 | subroutine search_backward(editor, buffer) |
| 213 | type(editor_state_t), intent(inout) :: editor |
| 214 | type(buffer_t), intent(inout) :: buffer |
| 215 | logical :: found |
| 216 | integer :: found_line, found_col |
| 217 | integer :: start_line, start_col |
| 218 | |
| 219 | if (.not. allocated(current_search_pattern)) return |
| 220 | |
| 221 | ! Re-count matches in case search options changed |
| 222 | call count_all_matches(buffer, current_search_pattern) |
| 223 | |
| 224 | ! Search backward from cursor position |
| 225 | start_line = editor%cursors(editor%active_cursor)%line |
| 226 | start_col = max(1, editor%cursors(editor%active_cursor)%column - 1) |
| 227 | |
| 228 | call find_prev_match(buffer, current_search_pattern, & |
| 229 | start_line, start_col, & |
| 230 | found, found_line, found_col) |
| 231 | |
| 232 | if (found) then |
| 233 | ! Move cursor to match and select it |
| 234 | editor%cursors(editor%active_cursor)%line = found_line |
| 235 | editor%cursors(editor%active_cursor)%column = found_col |
| 236 | editor%cursors(editor%active_cursor)%desired_column = found_col |
| 237 | |
| 238 | ! Create selection for the match |
| 239 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 240 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 241 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 242 | editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern) |
| 243 | |
| 244 | ! Update last search position |
| 245 | last_search_line = found_line |
| 246 | last_search_col = found_col |
| 247 | |
| 248 | ! Update viewport |
| 249 | call center_viewport_on_cursor(editor) |
| 250 | end if |
| 251 | end subroutine search_backward |
| 252 | |
| 253 | subroutine find_next_match(buffer, pattern, start_line, start_col, & |
| 254 | found, found_line, found_col) |
| 255 | type(buffer_t), intent(in) :: buffer |
| 256 | character(len=*), intent(in) :: pattern |
| 257 | integer, intent(in) :: start_line, start_col |
| 258 | logical, intent(out) :: found |
| 259 | integer, intent(out) :: found_line, found_col |
| 260 | character(len=:), allocatable :: line |
| 261 | integer :: line_count, current_line, pos |
| 262 | integer :: search_col |
| 263 | |
| 264 | found = .false. |
| 265 | found_line = 0 |
| 266 | found_col = 0 |
| 267 | line_count = buffer_get_line_count(buffer) |
| 268 | |
| 269 | ! Search from current position to end |
| 270 | do current_line = start_line, line_count |
| 271 | line = buffer_get_line(buffer, current_line) |
| 272 | |
| 273 | if (current_line == start_line) then |
| 274 | search_col = start_col + 1 |
| 275 | else |
| 276 | search_col = 1 |
| 277 | end if |
| 278 | |
| 279 | if (search_col <= len(line)) then |
| 280 | call find_pattern_in_line(line(search_col:), pattern, pos) |
| 281 | if (pos > 0) then |
| 282 | found_col = search_col + pos - 1 |
| 283 | |
| 284 | ! Check whole word constraint if enabled |
| 285 | if (whole_word) then |
| 286 | if (.not. is_whole_word_match(line, found_col, len(pattern))) then |
| 287 | ! Not a whole word match, continue searching |
| 288 | search_col = found_col + 1 |
| 289 | if (search_col <= len(line)) then |
| 290 | cycle |
| 291 | end if |
| 292 | end if |
| 293 | end if |
| 294 | |
| 295 | found = .true. |
| 296 | found_line = current_line |
| 297 | if (allocated(line)) deallocate(line) |
| 298 | ! Update match index |
| 299 | call update_match_index(buffer, pattern, found_line, found_col) |
| 300 | return |
| 301 | end if |
| 302 | end if |
| 303 | if (allocated(line)) deallocate(line) |
| 304 | end do |
| 305 | |
| 306 | ! Wrap around to beginning |
| 307 | do current_line = 1, start_line |
| 308 | line = buffer_get_line(buffer, current_line) |
| 309 | |
| 310 | if (current_line == start_line) then |
| 311 | ! Search only up to start position |
| 312 | if (start_col > 1) then |
| 313 | call find_pattern_in_line(line(1:start_col-1), pattern, pos) |
| 314 | else |
| 315 | pos = 0 |
| 316 | end if |
| 317 | else |
| 318 | call find_pattern_in_line(line, pattern, pos) |
| 319 | end if |
| 320 | |
| 321 | if (pos > 0) then |
| 322 | ! Check whole word constraint if enabled |
| 323 | if (whole_word) then |
| 324 | if (.not. is_whole_word_match(line, pos, len(pattern))) then |
| 325 | ! Not a whole word match, skip |
| 326 | if (allocated(line)) deallocate(line) |
| 327 | cycle |
| 328 | end if |
| 329 | end if |
| 330 | |
| 331 | found = .true. |
| 332 | found_line = current_line |
| 333 | found_col = pos |
| 334 | if (allocated(line)) deallocate(line) |
| 335 | ! Update match index |
| 336 | call update_match_index(buffer, pattern, found_line, found_col) |
| 337 | return |
| 338 | end if |
| 339 | |
| 340 | if (allocated(line)) deallocate(line) |
| 341 | end do |
| 342 | end subroutine find_next_match |
| 343 | |
| 344 | subroutine find_prev_match(buffer, pattern, start_line, start_col, & |
| 345 | found, found_line, found_col) |
| 346 | type(buffer_t), intent(in) :: buffer |
| 347 | character(len=*), intent(in) :: pattern |
| 348 | integer, intent(in) :: start_line, start_col |
| 349 | logical, intent(out) :: found |
| 350 | integer, intent(out) :: found_line, found_col |
| 351 | character(len=:), allocatable :: line |
| 352 | integer :: line_count, current_line |
| 353 | integer :: pos, last_pos, check_col |
| 354 | |
| 355 | found = .false. |
| 356 | found_line = 0 |
| 357 | found_col = 0 |
| 358 | line_count = buffer_get_line_count(buffer) |
| 359 | |
| 360 | ! Search backward from current position |
| 361 | do current_line = start_line, 1, -1 |
| 362 | line = buffer_get_line(buffer, current_line) |
| 363 | |
| 364 | if (current_line == start_line) then |
| 365 | check_col = min(start_col, len(line)) |
| 366 | else |
| 367 | check_col = len(line) |
| 368 | end if |
| 369 | |
| 370 | ! Find last occurrence before check_col |
| 371 | last_pos = 0 |
| 372 | pos = 1 |
| 373 | do while (pos <= check_col - len(pattern) + 1) |
| 374 | call find_pattern_in_line(line(pos:), pattern, found_col) |
| 375 | if (found_col > 0 .and. pos + found_col - 1 <= check_col) then |
| 376 | ! Check whole word constraint if enabled |
| 377 | if (whole_word) then |
| 378 | if (is_whole_word_match(line, pos + found_col - 1, len(pattern))) then |
| 379 | last_pos = pos + found_col - 1 |
| 380 | end if |
| 381 | else |
| 382 | last_pos = pos + found_col - 1 |
| 383 | end if |
| 384 | pos = pos + found_col ! Skip past this match |
| 385 | else |
| 386 | exit |
| 387 | end if |
| 388 | end do |
| 389 | |
| 390 | if (last_pos > 0) then |
| 391 | found = .true. |
| 392 | found_line = current_line |
| 393 | found_col = last_pos |
| 394 | if (allocated(line)) deallocate(line) |
| 395 | ! Update match index |
| 396 | call update_match_index(buffer, pattern, found_line, found_col) |
| 397 | return |
| 398 | end if |
| 399 | |
| 400 | if (allocated(line)) deallocate(line) |
| 401 | end do |
| 402 | |
| 403 | ! Wrap around from end |
| 404 | do current_line = line_count, start_line, -1 |
| 405 | if (current_line <= start_line) exit |
| 406 | |
| 407 | line = buffer_get_line(buffer, current_line) |
| 408 | |
| 409 | ! Find last occurrence in line |
| 410 | last_pos = 0 |
| 411 | pos = 1 |
| 412 | do while (pos <= len(line) - len(pattern) + 1) |
| 413 | call find_pattern_in_line(line(pos:), pattern, found_col) |
| 414 | if (found_col > 0) then |
| 415 | ! Check whole word constraint if enabled |
| 416 | if (whole_word) then |
| 417 | if (is_whole_word_match(line, pos + found_col - 1, len(pattern))) then |
| 418 | last_pos = pos + found_col - 1 |
| 419 | end if |
| 420 | else |
| 421 | last_pos = pos + found_col - 1 |
| 422 | end if |
| 423 | pos = pos + found_col ! Skip past this match |
| 424 | else |
| 425 | exit |
| 426 | end if |
| 427 | end do |
| 428 | |
| 429 | if (last_pos > 0) then |
| 430 | found = .true. |
| 431 | found_line = current_line |
| 432 | found_col = last_pos |
| 433 | if (allocated(line)) deallocate(line) |
| 434 | ! Update match index |
| 435 | call update_match_index(buffer, pattern, found_line, found_col) |
| 436 | return |
| 437 | end if |
| 438 | |
| 439 | if (allocated(line)) deallocate(line) |
| 440 | end do |
| 441 | end subroutine find_prev_match |
| 442 | |
| 443 | subroutine center_viewport_on_cursor(editor) |
| 444 | type(editor_state_t), intent(inout) :: editor |
| 445 | integer :: cursor_line |
| 446 | integer :: viewport_height |
| 447 | |
| 448 | cursor_line = editor%cursors(editor%active_cursor)%line |
| 449 | viewport_height = editor%screen_rows - 2 ! Account for status bar |
| 450 | |
| 451 | ! Center the cursor in the viewport |
| 452 | editor%viewport_line = max(1, cursor_line - viewport_height / 2) |
| 453 | end subroutine center_viewport_on_cursor |
| 454 | |
| 455 | subroutine clear_search_pattern() |
| 456 | if (allocated(current_search_pattern)) then |
| 457 | deallocate(current_search_pattern) |
| 458 | end if |
| 459 | last_search_line = 1 |
| 460 | last_search_col = 1 |
| 461 | end subroutine clear_search_pattern |
| 462 | |
| 463 | subroutine build_search_prompt(prompt) |
| 464 | character(len=*), intent(out) :: prompt |
| 465 | character(len=64) :: options |
| 466 | character(len=32) :: count_str |
| 467 | |
| 468 | options = '' |
| 469 | |
| 470 | ! Build options string |
| 471 | if (case_sensitive) then |
| 472 | options = trim(options) // '[Cc]' |
| 473 | else |
| 474 | options = trim(options) // '[cc]' |
| 475 | end if |
| 476 | |
| 477 | if (whole_word) then |
| 478 | options = trim(options) // '[Ww]' |
| 479 | else |
| 480 | options = trim(options) // '[ww]' |
| 481 | end if |
| 482 | |
| 483 | ! Add match count if we have a pattern and matches |
| 484 | if (allocated(current_search_pattern) .and. total_matches > 0) then |
| 485 | write(count_str, '(I0,A,I0)') current_match_index, ' of ', total_matches |
| 486 | options = trim(options) // ' (' // trim(count_str) // ')' |
| 487 | end if |
| 488 | |
| 489 | ! Build full prompt with ESC indicator |
| 490 | if (len_trim(options) > 0) then |
| 491 | prompt = 'Search ' // trim(options) // ' | ESC:exit: ' |
| 492 | else |
| 493 | prompt = 'Search | ESC:exit: ' |
| 494 | end if |
| 495 | end subroutine build_search_prompt |
| 496 | |
| 497 | subroutine find_pattern_in_line(line, pattern, pos) |
| 498 | character(len=*), intent(in) :: line |
| 499 | character(len=*), intent(in) :: pattern |
| 500 | integer, intent(out) :: pos |
| 501 | character(len=:), allocatable :: search_line |
| 502 | character(len=:), allocatable :: search_pattern |
| 503 | integer :: i |
| 504 | |
| 505 | pos = 0 |
| 506 | |
| 507 | if (len(line) == 0 .or. len(pattern) == 0) return |
| 508 | if (len(pattern) > len(line)) return |
| 509 | |
| 510 | ! Handle case sensitivity |
| 511 | if (case_sensitive) then |
| 512 | ! Direct search |
| 513 | pos = index(line, pattern) |
| 514 | else |
| 515 | ! Case-insensitive search - convert both to lowercase |
| 516 | allocate(character(len=len(line)) :: search_line) |
| 517 | allocate(character(len=len(pattern)) :: search_pattern) |
| 518 | |
| 519 | ! Convert line to lowercase |
| 520 | do i = 1, len(line) |
| 521 | if (iachar(line(i:i)) >= iachar('A') .and. & |
| 522 | iachar(line(i:i)) <= iachar('Z')) then |
| 523 | search_line(i:i) = char(iachar(line(i:i)) + 32) |
| 524 | else |
| 525 | search_line(i:i) = line(i:i) |
| 526 | end if |
| 527 | end do |
| 528 | |
| 529 | ! Convert pattern to lowercase |
| 530 | do i = 1, len(pattern) |
| 531 | if (iachar(pattern(i:i)) >= iachar('A') .and. & |
| 532 | iachar(pattern(i:i)) <= iachar('Z')) then |
| 533 | search_pattern(i:i) = char(iachar(pattern(i:i)) + 32) |
| 534 | else |
| 535 | search_pattern(i:i) = pattern(i:i) |
| 536 | end if |
| 537 | end do |
| 538 | |
| 539 | pos = index(search_line, search_pattern) |
| 540 | |
| 541 | deallocate(search_line) |
| 542 | deallocate(search_pattern) |
| 543 | end if |
| 544 | end subroutine find_pattern_in_line |
| 545 | |
| 546 | logical function is_whole_word_match(line, start_pos, pattern_len) |
| 547 | character(len=*), intent(in) :: line |
| 548 | integer, intent(in) :: start_pos, pattern_len |
| 549 | logical :: word_start, word_end |
| 550 | integer :: end_pos |
| 551 | |
| 552 | end_pos = start_pos + pattern_len - 1 |
| 553 | |
| 554 | ! Check if match is at word boundary (start) |
| 555 | if (start_pos == 1) then |
| 556 | word_start = .true. |
| 557 | else |
| 558 | word_start = .not. is_word_char(line(start_pos-1:start_pos-1)) |
| 559 | end if |
| 560 | |
| 561 | ! Check if match is at word boundary (end) |
| 562 | if (end_pos == len(line)) then |
| 563 | word_end = .true. |
| 564 | else |
| 565 | word_end = .not. is_word_char(line(end_pos+1:end_pos+1)) |
| 566 | end if |
| 567 | |
| 568 | is_whole_word_match = word_start .and. word_end |
| 569 | end function is_whole_word_match |
| 570 | |
| 571 | logical function is_word_char(ch) |
| 572 | character(len=1), intent(in) :: ch |
| 573 | integer :: ascii_val |
| 574 | |
| 575 | ascii_val = iachar(ch) |
| 576 | |
| 577 | ! Check if character is alphanumeric or underscore |
| 578 | is_word_char = (ascii_val >= iachar('A') .and. ascii_val <= iachar('Z')) .or. & |
| 579 | (ascii_val >= iachar('a') .and. ascii_val <= iachar('z')) .or. & |
| 580 | (ascii_val >= iachar('0') .and. ascii_val <= iachar('9')) .or. & |
| 581 | (ch == '_') |
| 582 | end function is_word_char |
| 583 | |
| 584 | subroutine count_all_matches(buffer, pattern) |
| 585 | type(buffer_t), intent(in) :: buffer |
| 586 | character(len=*), intent(in) :: pattern |
| 587 | character(len=:), allocatable :: line |
| 588 | integer :: line_count, current_line, pos, col |
| 589 | integer :: match_count |
| 590 | |
| 591 | match_count = 0 |
| 592 | line_count = buffer_get_line_count(buffer) |
| 593 | |
| 594 | ! Go through all lines and count matches |
| 595 | do current_line = 1, line_count |
| 596 | line = buffer_get_line(buffer, current_line) |
| 597 | col = 1 |
| 598 | |
| 599 | do while (col <= len(line)) |
| 600 | call find_pattern_in_line(line(col:), pattern, pos) |
| 601 | if (pos > 0) then |
| 602 | pos = col + pos - 1 |
| 603 | ! Check whole word constraint if enabled |
| 604 | if (whole_word) then |
| 605 | if (is_whole_word_match(line, pos, len(pattern))) then |
| 606 | match_count = match_count + 1 |
| 607 | end if |
| 608 | else |
| 609 | match_count = match_count + 1 |
| 610 | end if |
| 611 | col = pos + 1 |
| 612 | else |
| 613 | exit |
| 614 | end if |
| 615 | end do |
| 616 | |
| 617 | if (allocated(line)) deallocate(line) |
| 618 | end do |
| 619 | |
| 620 | total_matches = match_count |
| 621 | current_match_index = 0 ! Will be set when we find first match |
| 622 | end subroutine count_all_matches |
| 623 | |
| 624 | subroutine perform_incremental_search(editor, buffer, pattern) |
| 625 | type(editor_state_t), intent(inout) :: editor |
| 626 | type(buffer_t), intent(in) :: buffer |
| 627 | character(len=*), intent(in) :: pattern |
| 628 | logical :: found |
| 629 | integer :: found_line, found_col |
| 630 | integer :: start_line, start_col |
| 631 | |
| 632 | ! Search from current cursor position |
| 633 | start_line = editor%cursors(editor%active_cursor)%line |
| 634 | start_col = editor%cursors(editor%active_cursor)%column |
| 635 | |
| 636 | ! Find the first match |
| 637 | call find_next_match(buffer, pattern, start_line, start_col, & |
| 638 | found, found_line, found_col) |
| 639 | |
| 640 | if (found) then |
| 641 | ! Update viewport to show the match (but don't move cursor yet) |
| 642 | editor%viewport_line = max(1, found_line - editor%screen_rows / 2) |
| 643 | |
| 644 | ! Store the match position for highlighting (could be used for visual feedback) |
| 645 | last_search_line = found_line |
| 646 | last_search_col = found_col |
| 647 | end if |
| 648 | end subroutine perform_incremental_search |
| 649 | |
| 650 | subroutine update_match_index(buffer, pattern, match_line, match_col) |
| 651 | type(buffer_t), intent(in) :: buffer |
| 652 | character(len=*), intent(in) :: pattern |
| 653 | integer, intent(in) :: match_line, match_col |
| 654 | character(len=:), allocatable :: line |
| 655 | integer :: line_count, current_line, pos, col |
| 656 | integer :: match_count |
| 657 | |
| 658 | match_count = 0 |
| 659 | line_count = buffer_get_line_count(buffer) |
| 660 | |
| 661 | ! Count matches until we reach the current one |
| 662 | do current_line = 1, line_count |
| 663 | line = buffer_get_line(buffer, current_line) |
| 664 | col = 1 |
| 665 | |
| 666 | do while (col <= len(line)) |
| 667 | call find_pattern_in_line(line(col:), pattern, pos) |
| 668 | if (pos > 0) then |
| 669 | pos = col + pos - 1 |
| 670 | ! Check whole word constraint if enabled |
| 671 | if (whole_word) then |
| 672 | if (is_whole_word_match(line, pos, len(pattern))) then |
| 673 | match_count = match_count + 1 |
| 674 | if (current_line == match_line .and. pos == match_col) then |
| 675 | current_match_index = match_count |
| 676 | if (allocated(line)) deallocate(line) |
| 677 | return |
| 678 | end if |
| 679 | end if |
| 680 | else |
| 681 | match_count = match_count + 1 |
| 682 | if (current_line == match_line .and. pos == match_col) then |
| 683 | current_match_index = match_count |
| 684 | if (allocated(line)) deallocate(line) |
| 685 | return |
| 686 | end if |
| 687 | end if |
| 688 | col = pos + 1 |
| 689 | else |
| 690 | exit |
| 691 | end if |
| 692 | end do |
| 693 | |
| 694 | if (allocated(line)) deallocate(line) |
| 695 | end do |
| 696 | end subroutine update_match_index |
| 697 | |
| 698 | end module search_prompt_module |