Fortran · 58770 bytes Raw Blame History
1 module unified_search_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, sync_editor_to_pane
5 use text_buffer_module
6 use regex_module
7 implicit none
8 private
9
10 public :: show_unified_search_prompt
11 public :: current_search_pattern, clear_search_pattern
12 public :: find_next_match, find_prev_match, center_viewport_on_cursor
13 public :: get_matches_on_line, search_mode_active
14
15 ! Module variables for search/replace state
16 character(len=:), allocatable :: current_search_pattern
17 character(len=:), allocatable :: current_replace_text
18 integer :: last_search_line = 1
19 integer :: last_search_col = 1
20
21 ! Search options
22 logical :: case_sensitive = .false.
23 logical :: whole_word = .false.
24 logical :: use_regex = .false.
25 integer :: total_matches = 0
26 integer :: current_match_index = 0
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
34 ! Active search mode - persists after first search
35 logical :: search_mode_active = .false.
36
37 ! Track last search parameters to detect changes
38 character(len=:), allocatable :: last_search_pattern
39 logical :: last_case_sensitive = .false.
40 logical :: last_whole_word = .false.
41 logical :: last_use_regex = .false.
42
43 ! Field focus (1 = find, 2 = replace)
44 integer :: active_field = 1
45
46 ! Search history
47 integer, parameter :: MAX_HISTORY = 20
48 character(len=256), dimension(MAX_HISTORY) :: search_history
49 integer :: history_count = 0
50 integer :: history_index = 0 ! Current position when navigating history
51
52 ! Search in selection mode
53 logical :: search_in_selection = .false.
54 integer :: selection_start_line = 1
55 integer :: selection_start_col = 1
56 integer :: selection_end_line = 1
57 integer :: selection_end_col = 1
58
59 contains
60
61 subroutine show_unified_search_prompt(editor, buffer)
62 type(editor_state_t), intent(inout) :: editor
63 type(buffer_t), intent(inout) :: buffer
64 character(len=256) :: find_buffer, replace_buffer
65 character(len=256) :: prompt
66 integer :: find_pos, replace_pos, ch
67 integer :: temp_line, temp_col
68 logical :: in_alt_sequence
69
70 ! Initialize
71 find_buffer = ''
72 replace_buffer = ''
73 find_pos = 0
74 replace_pos = 0
75 active_field = 1 ! Start with find field
76 in_alt_sequence = .false.
77
78 ! Check if there's an active selection for search-in-selection mode
79 if (editor%cursors(editor%active_cursor)%has_selection) then
80 search_in_selection = .true.
81 selection_start_line = editor%cursors(editor%active_cursor)%selection_start_line
82 selection_start_col = editor%cursors(editor%active_cursor)%selection_start_col
83 selection_end_line = editor%cursors(editor%active_cursor)%line
84 selection_end_col = editor%cursors(editor%active_cursor)%column
85 ! Ensure start comes before end
86 if (selection_start_line > selection_end_line .or. &
87 (selection_start_line == selection_end_line .and. selection_start_col > selection_end_col)) then
88 ! Swap
89 temp_line = selection_start_line
90 temp_col = selection_start_col
91 selection_start_line = selection_end_line
92 selection_start_col = selection_end_col
93 selection_end_line = temp_line
94 selection_end_col = temp_col
95 end if
96 else
97 search_in_selection = .false.
98 end if
99
100 ! If we have existing patterns, load them
101 if (allocated(current_search_pattern)) then
102 find_buffer = current_search_pattern
103 find_pos = len(current_search_pattern)
104 end if
105 if (allocated(current_replace_text)) then
106 replace_buffer = current_replace_text
107 replace_pos = len(current_replace_text)
108 end if
109
110 ! Build and display prompt
111 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
112 call display_prompt(editor, prompt, find_pos, replace_pos)
113
114 ! Input loop
115 do
116 ch = terminal_read_char()
117
118 if (ch == -1) then
119 cycle
120 else if (ch == 27) then ! ESC or Alt sequence
121 in_alt_sequence = .true.
122 ch = terminal_read_char()
123
124 if (ch == -1 .or. ch == 27) then
125 ! Standalone ESC - exit search mode
126 ! DEBUG: Log ESC pressed
127 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
128 write(99, '(A)') 'ESC: Exiting search mode, about to return'
129 close(99)
130 search_mode_active = .false.
131 exit
132 else if (ch == iachar('[')) then
133 ! Arrow keys or mouse events
134 ch = terminal_read_char()
135 if (ch == iachar('<')) then
136 ! Mouse event - consume until 'M' or 'm'
137 do
138 ch = terminal_read_char()
139 if (ch == iachar('M') .or. ch == iachar('m') .or. ch == -1) exit
140 end do
141 in_alt_sequence = .false.
142 cycle
143 else if (ch == iachar('A')) then
144 ! Up arrow - navigate history backward (older)
145 if (active_field == 1) then ! Only in find field
146 call navigate_history_up(find_buffer, find_pos)
147 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
148 call display_prompt(editor, prompt, find_pos, replace_pos)
149 end if
150 else if (ch == iachar('B')) then
151 ! Down arrow - navigate history forward (newer)
152 if (active_field == 1) then ! Only in find field
153 call navigate_history_down(find_buffer, find_pos)
154 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
155 call display_prompt(editor, prompt, find_pos, replace_pos)
156 end if
157 end if
158 ! Not a mouse event, fall through
159 in_alt_sequence = .false.
160 cycle
161 else if (ch == iachar('c') .or. ch == iachar('C')) then
162 ! Alt+C - toggle case sensitive
163 case_sensitive = .not. case_sensitive
164 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
165 call display_prompt(editor, prompt, find_pos, replace_pos)
166 in_alt_sequence = .false.
167 else if (ch == iachar('w') .or. ch == iachar('W')) then
168 ! Alt+W - toggle whole word
169 whole_word = .not. whole_word
170 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
171 call display_prompt(editor, prompt, find_pos, replace_pos)
172 in_alt_sequence = .false.
173 else if (ch == iachar('r') .or. ch == iachar('R')) then
174 ! Alt+R - toggle regex mode
175 use_regex = .not. use_regex
176 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
177 call display_prompt(editor, prompt, find_pos, replace_pos)
178 in_alt_sequence = .false.
179 else if (ch == iachar('s') .or. ch == iachar('S')) then
180 ! Alt+S - toggle search in selection
181 search_in_selection = .not. search_in_selection
182 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
183 call display_prompt(editor, prompt, find_pos, replace_pos)
184 in_alt_sequence = .false.
185 else
186 in_alt_sequence = .false.
187 end if
188 else if (ch == 9) then ! Tab - switch fields
189 ! DEBUG: Check selection when Tab is pressed
190 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
191 write(99, '(A,L1)') 'TAB: has_sel=', editor%cursors(editor%active_cursor)%has_selection
192 close(99)
193
194 if (active_field == 1) then
195 active_field = 2
196 else
197 active_field = 1
198 end if
199 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
200 call display_prompt(editor, prompt, find_pos, replace_pos)
201 else if (ch == 6) then ! Ctrl+F - find next
202 if (find_pos > 0) then
203 ! Save search pattern
204 if (allocated(current_search_pattern)) deallocate(current_search_pattern)
205 allocate(character(len=find_pos) :: current_search_pattern)
206 current_search_pattern = find_buffer(1:find_pos)
207
208 ! Add to search history
209 call add_to_search_history(current_search_pattern)
210
211 ! Check if search parameters changed - if so, reset search mode
212 if (search_mode_active) then
213 if (.not. allocated(last_search_pattern) .or. &
214 current_search_pattern /= last_search_pattern .or. &
215 case_sensitive .neqv. last_case_sensitive .or. &
216 whole_word .neqv. last_whole_word .or. &
217 use_regex .neqv. last_use_regex) then
218 ! Parameters changed - treat as new search
219 search_mode_active = .false.
220 end if
221 end if
222
223 if (.not. search_mode_active) then
224 ! First search - count and find
225 search_mode_active = .true.
226 call count_all_matches(buffer, current_search_pattern)
227 call perform_search(editor, buffer, current_search_pattern)
228
229 ! Save current parameters
230 if (allocated(last_search_pattern)) deallocate(last_search_pattern)
231 allocate(character(len=len(current_search_pattern)) :: last_search_pattern)
232 last_search_pattern = current_search_pattern
233 last_case_sensitive = case_sensitive
234 last_whole_word = whole_word
235 last_use_regex = use_regex
236 else
237 ! Cycle to next match
238 call search_forward(editor, buffer)
239 end if
240
241 ! Note: Screen re-rendering happens in command_handler after search exits
242 ! The match highlighting and cursor position will be visible after exiting search
243
244 ! Update prompt with match count
245 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
246 call display_prompt(editor, prompt, find_pos, replace_pos)
247 end if
248 else if (ch == 18) then ! Ctrl+R - replace current and advance
249 if (find_pos > 0 .and. replace_pos >= 0) then
250 ! Save patterns
251 if (allocated(current_search_pattern)) deallocate(current_search_pattern)
252 if (allocated(current_replace_text)) deallocate(current_replace_text)
253 allocate(character(len=find_pos) :: current_search_pattern)
254 allocate(character(len=replace_pos) :: current_replace_text)
255 current_search_pattern = find_buffer(1:find_pos)
256 current_replace_text = replace_buffer(1:replace_pos)
257
258 ! Perform replacement
259 call replace_current_and_advance(editor, buffer)
260
261 ! Clear old prompt and re-render everything
262 call terminal_move_cursor(editor%screen_rows, 1)
263 call terminal_write(repeat(' ', editor%screen_cols))
264
265 ! Update prompt with new match count
266 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
267 call display_prompt(editor, prompt, find_pos, replace_pos)
268 end if
269 else if (ch == 1) then ! Ctrl+A - replace all
270 if (find_pos > 0 .and. replace_pos >= 0) then
271 ! Save patterns
272 if (allocated(current_search_pattern)) deallocate(current_search_pattern)
273 if (allocated(current_replace_text)) deallocate(current_replace_text)
274 allocate(character(len=find_pos) :: current_search_pattern)
275 allocate(character(len=replace_pos) :: current_replace_text)
276 current_search_pattern = find_buffer(1:find_pos)
277 current_replace_text = replace_buffer(1:replace_pos)
278
279 call replace_all_matches(editor, buffer)
280
281 ! Exit after replace all
282 search_mode_active = .false.
283 exit
284 end if
285 else if (ch == 13 .or. ch == 10) then ! Enter - find first match and exit
286 ! If we have a search pattern, perform search first if needed
287 if (find_pos > 0) then
288 ! Save search pattern
289 if (allocated(current_search_pattern)) deallocate(current_search_pattern)
290 allocate(character(len=find_pos) :: current_search_pattern)
291 current_search_pattern = find_buffer(1:find_pos)
292
293 ! Add to search history
294 call add_to_search_history(current_search_pattern)
295
296 ! If no selection yet (haven't searched), perform the search
297 if (.not. editor%cursors(editor%active_cursor)%has_selection) then
298 search_mode_active = .true.
299 call count_all_matches(buffer, current_search_pattern)
300 call perform_search(editor, buffer, current_search_pattern)
301
302 ! Save current parameters
303 if (allocated(last_search_pattern)) deallocate(last_search_pattern)
304 allocate(character(len=len(current_search_pattern)) :: last_search_pattern)
305 last_search_pattern = current_search_pattern
306 last_case_sensitive = case_sensitive
307 last_whole_word = whole_word
308 last_use_regex = use_regex
309 end if
310 end if
311
312 ! Move cursor to START of match (not end)
313 if (editor%cursors(editor%active_cursor)%has_selection) then
314 editor%cursors(editor%active_cursor)%line = &
315 editor%cursors(editor%active_cursor)%selection_start_line
316 editor%cursors(editor%active_cursor)%column = &
317 editor%cursors(editor%active_cursor)%selection_start_col
318 editor%cursors(editor%active_cursor)%desired_column = &
319 editor%cursors(editor%active_cursor)%selection_start_col
320 ! Clear selection so cursor is at start, not selecting
321 editor%cursors(editor%active_cursor)%has_selection = .false.
322 ! Sync cursor back to pane (important for pane system!)
323 call sync_editor_to_pane(editor)
324 end if
325 exit
326 else if (ch == 127 .or. ch == 8) then ! Backspace
327 if (active_field == 1 .and. find_pos > 0) then
328 find_pos = find_pos - 1
329 else if (active_field == 2 .and. replace_pos > 0) then
330 replace_pos = replace_pos - 1
331 end if
332 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
333 call display_prompt(editor, prompt, find_pos, replace_pos)
334 else if (ch >= 32 .and. ch <= 126) then ! Printable characters
335 if (active_field == 1 .and. find_pos < 256) then
336 find_pos = find_pos + 1
337 find_buffer(find_pos:find_pos) = char(ch)
338 else if (active_field == 2 .and. replace_pos < 256) then
339 replace_pos = replace_pos + 1
340 replace_buffer(replace_pos:replace_pos) = char(ch)
341 end if
342 call build_unified_prompt(prompt, find_buffer, find_pos, replace_buffer, replace_pos)
343 call display_prompt(editor, prompt, find_pos, replace_pos)
344 end if
345 end do
346
347 ! Clean up - clear the prompt line
348 call terminal_move_cursor(editor%screen_rows, 1)
349 call terminal_write(repeat(' ', editor%screen_cols))
350 ! Don't hide cursor - let the main render loop handle cursor display
351 end subroutine show_unified_search_prompt
352
353 subroutine build_unified_prompt(prompt, find_text, find_len, replace_text, replace_len)
354 character(len=*), intent(out) :: prompt
355 character(len=*), intent(in) :: find_text, replace_text
356 integer, intent(in) :: find_len, replace_len
357 character(len=64) :: options, count_str
358 character(len=25) :: find_field, replace_field
359 character(len=1) :: esc = char(27)
360 integer :: i
361 integer, parameter :: FIELD_WIDTH = 20 ! Reduced from 30 for narrower terminals
362
363 ! Build options string with all three toggles
364 options = ''
365 if (case_sensitive) then
366 options = trim(options) // '[Cc]'
367 else
368 options = trim(options) // '[cc]'
369 end if
370 if (whole_word) then
371 options = trim(options) // '[Ww]'
372 else
373 options = trim(options) // '[ww]'
374 end if
375 if (use_regex) then
376 options = trim(options) // '[Rr]'
377 else
378 options = trim(options) // '[rr]'
379 end if
380 if (search_in_selection) then
381 options = trim(options) // '[Ss]'
382 else
383 options = trim(options) // '[ss]'
384 end if
385
386 ! Add match count if available
387 if (allocated(current_search_pattern) .and. total_matches > 0) then
388 write(count_str, '(A,I0,A,I0,A)') ' (', current_match_index, '/', total_matches, ')'
389 options = trim(options) // trim(count_str)
390 end if
391
392 ! Build fixed-width fields with padding (20 chars each for compact display)
393 find_field = find_text(1:min(find_len, FIELD_WIDTH))
394 do i = find_len + 1, FIELD_WIDTH
395 find_field(i:i) = ' '
396 end do
397
398 replace_field = replace_text(1:min(replace_len, FIELD_WIDTH))
399 do i = replace_len + 1, FIELD_WIDTH
400 replace_field(i:i) = ' '
401 end do
402
403 ! Build unified prompt with reverse video highlighting for active field
404 if (active_field == 1) then
405 ! Find field active (reverse video)
406 write(prompt, '(9A)') &
407 esc, '[7m[f]:', find_field, esc, '[27m /[r]:', &
408 replace_field, ' ', trim(options), ' RET:go ESC:exit'
409 else
410 ! Replace field active (reverse video)
411 write(prompt, '(10A)') &
412 '[f]:', find_field, ' ', esc, '[7m/[r]:', &
413 replace_field, esc, '[27m ', trim(options), ' RET:go ESC:exit'
414 end if
415 end subroutine build_unified_prompt
416
417 subroutine display_prompt(editor, prompt, find_len, replace_len)
418 type(editor_state_t), intent(in) :: editor
419 character(len=*), intent(in) :: prompt
420 integer, intent(in) :: find_len, replace_len
421 integer :: cursor_pos
422
423 ! Hide cursor during redraw to prevent flicker
424 call terminal_hide_cursor()
425
426 ! Clear the entire status line
427 call terminal_move_cursor(editor%screen_rows, 1)
428 call terminal_write(repeat(' ', editor%screen_cols))
429
430 ! Move back to start and write prompt
431 call terminal_move_cursor(editor%screen_rows, 1)
432 call terminal_write(trim(prompt))
433
434 ! Calculate cursor position within the active field
435 ! Account for escape sequences which don't take screen space
436 ! Compact layout: "[f]: <20 chars> /[r]: <20 chars> ..."
437 if (active_field == 1) then
438 ! Cursor in find field: "[f]:" = 4 visible chars
439 cursor_pos = 4 + find_len + 1
440 else
441 ! Cursor in replace field
442 ! "[f]:" (4) + field (20) + " /[r]:" (6)
443 cursor_pos = 4 + 20 + 6 + replace_len + 1
444 end if
445
446 ! Position cursor and show it
447 call terminal_move_cursor(editor%screen_rows, cursor_pos)
448 call terminal_show_cursor()
449 end subroutine display_prompt
450
451 subroutine perform_search(editor, buffer, pattern)
452 type(editor_state_t), intent(inout) :: editor
453 type(buffer_t), intent(inout) :: buffer
454 character(len=*), intent(in) :: pattern
455 logical :: found
456 integer :: found_line, found_col
457
458 ! Compile regex if in regex mode
459 if (use_regex) then
460 ! Free old regex if any
461 if (compiled_regex_id >= 0) then
462 call regex_free(compiled_regex_id)
463 end if
464 ! Compile new pattern
465 compiled_regex_id = regex_compile(pattern, case_sensitive)
466 if (compiled_regex_id < 0) then
467 ! Regex compilation failed - could show error, for now just skip
468 return
469 end if
470 end if
471
472 call find_next_match(buffer, pattern, &
473 editor%cursors(editor%active_cursor)%line, &
474 editor%cursors(editor%active_cursor)%column, &
475 found, found_line, found_col)
476
477 if (found) then
478 editor%cursors(editor%active_cursor)%line = found_line
479 editor%cursors(editor%active_cursor)%column = found_col
480 editor%cursors(editor%active_cursor)%desired_column = found_col
481
482 ! Create selection
483 ! For regex, use the match length from last search
484 ! For normal search, use pattern length
485 editor%cursors(editor%active_cursor)%has_selection = .true.
486 editor%cursors(editor%active_cursor)%selection_start_line = found_line
487 editor%cursors(editor%active_cursor)%selection_start_col = found_col
488 if (use_regex .and. last_match_length > 0) then
489 editor%cursors(editor%active_cursor)%column = found_col + last_match_length
490 else
491 editor%cursors(editor%active_cursor)%column = found_col + len(pattern)
492 end if
493
494 last_search_line = found_line
495 last_search_col = found_col
496
497 ! Center viewport on the found match FIRST
498 call center_viewport_on_cursor(editor)
499
500 ! THEN sync cursor and viewport to pane so rendering shows updated state
501 call sync_editor_to_pane(editor)
502 end if
503 end subroutine perform_search
504
505 subroutine search_forward(editor, buffer)
506 type(editor_state_t), intent(inout) :: editor
507 type(buffer_t), intent(inout) :: buffer
508 logical :: found
509 integer :: found_line, found_col
510 integer :: start_line, start_col
511
512 if (.not. allocated(current_search_pattern)) return
513
514 ! Re-count matches
515 call count_all_matches(buffer, current_search_pattern)
516
517 ! Search from cursor position
518 start_line = editor%cursors(editor%active_cursor)%line
519 start_col = editor%cursors(editor%active_cursor)%column
520
521 call find_next_match(buffer, current_search_pattern, &
522 start_line, start_col, &
523 found, found_line, found_col)
524
525 if (found) then
526 editor%cursors(editor%active_cursor)%line = found_line
527 editor%cursors(editor%active_cursor)%column = found_col
528 editor%cursors(editor%active_cursor)%desired_column = found_col
529
530 editor%cursors(editor%active_cursor)%has_selection = .true.
531 editor%cursors(editor%active_cursor)%selection_start_line = found_line
532 editor%cursors(editor%active_cursor)%selection_start_col = found_col
533
534 ! Use last_match_length for regex (which was set by find_next_match)
535 if (use_regex .and. last_match_length > 0) then
536 editor%cursors(editor%active_cursor)%column = found_col + last_match_length
537 else
538 editor%cursors(editor%active_cursor)%column = found_col + len(current_search_pattern)
539 end if
540
541 last_search_line = found_line
542 last_search_col = found_col
543
544 ! Center viewport on the found match FIRST
545 call center_viewport_on_cursor(editor)
546
547 ! THEN sync cursor and viewport to pane
548 call sync_editor_to_pane(editor)
549 end if
550 end subroutine search_forward
551
552 subroutine replace_current_and_advance(editor, buffer)
553 type(editor_state_t), intent(inout) :: editor
554 type(buffer_t), intent(inout) :: buffer
555 integer :: match_len
556 integer :: tab_i, pane_i
557
558 if (.not. allocated(current_search_pattern)) return
559 if (.not. allocated(current_replace_text)) return
560
561 ! DEBUG: Write to file
562 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
563 write(99, '(A,I0,A,I0,A,L1)') 'REPLACE: cursor at line=', &
564 editor%cursors(editor%active_cursor)%line, ' col=', &
565 editor%cursors(editor%active_cursor)%column, ' has_sel=', &
566 editor%cursors(editor%active_cursor)%has_selection
567 close(99)
568
569 ! Check if cursor has selection
570 if (editor%cursors(editor%active_cursor)%has_selection) then
571 ! DEBUG: Write selection info
572 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
573 write(99, '(A,I0,A,I0)') 'REPLACE: selection at line=', &
574 editor%cursors(editor%active_cursor)%selection_start_line, ' col=', &
575 editor%cursors(editor%active_cursor)%selection_start_col
576 close(99)
577 ! Calculate match length from selection
578 if (last_match_length > 0) then
579 match_len = last_match_length
580 else
581 match_len = editor%cursors(editor%active_cursor)%column - &
582 editor%cursors(editor%active_cursor)%selection_start_col
583 end if
584
585 ! DEBUG: About to perform replacement
586 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
587 write(99, '(A,I0,A,A,A,I0)') 'REPLACE: Calling perform_replacement with match_len=', &
588 match_len, ' replace_text="', trim(current_replace_text), '" at line=', &
589 editor%cursors(editor%active_cursor)%selection_start_line
590 close(99)
591
592 ! Perform replacement on parameter buffer
593 call perform_replacement(buffer, editor%cursors(editor%active_cursor), &
594 current_replace_text, match_len)
595
596 ! CRITICAL: Also update the pane's buffer if using panes
597 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
598 tab_i = editor%active_tab_index
599 if (allocated(editor%tabs(tab_i)%panes)) then
600 pane_i = editor%tabs(tab_i)%active_pane_index
601 if (pane_i > 0 .and. pane_i <= size(editor%tabs(tab_i)%panes)) then
602 ! Copy the modified buffer to the pane's buffer
603 call copy_buffer(editor%tabs(tab_i)%panes(pane_i)%buffer, buffer)
604 end if
605 end if
606 end if
607
608 ! Create selection highlighting the replacement text
609 ! Cursor is already at end of replacement from perform_replacement
610 editor%cursors(editor%active_cursor)%has_selection = .true.
611 editor%cursors(editor%active_cursor)%selection_start_line = &
612 editor%cursors(editor%active_cursor)%line
613 editor%cursors(editor%active_cursor)%selection_start_col = &
614 editor%cursors(editor%active_cursor)%column - len(current_replace_text)
615
616 ! Sync cursor to pane (important!)
617 call sync_editor_to_pane(editor)
618
619 ! Re-count matches after replacement
620 call count_all_matches(buffer, current_search_pattern)
621
622 ! Note: render_screen should be called by the caller
623 end if
624 end subroutine replace_current_and_advance
625
626 subroutine replace_all_matches(editor, buffer)
627 type(editor_state_t), intent(inout) :: editor
628 type(buffer_t), intent(inout) :: buffer
629 logical :: found
630 integer :: found_line, found_col, replace_count
631 type(cursor_t) :: temp_cursor
632
633 if (.not. allocated(current_search_pattern)) return
634 if (.not. allocated(current_replace_text)) return
635
636 replace_count = 0
637 temp_cursor = editor%cursors(editor%active_cursor)
638
639 ! Start from beginning
640 temp_cursor%line = 1
641 temp_cursor%column = 0
642
643 do
644 call find_next_match(buffer, current_search_pattern, &
645 temp_cursor%line, temp_cursor%column, &
646 found, found_line, found_col)
647
648 if (.not. found) exit
649
650 ! Move cursor to match and select it
651 temp_cursor%line = found_line
652 temp_cursor%column = found_col
653 temp_cursor%has_selection = .true.
654 temp_cursor%selection_start_line = found_line
655 temp_cursor%selection_start_col = found_col
656 temp_cursor%column = found_col + last_match_length
657
658 ! Replace
659 call perform_replacement(buffer, temp_cursor, current_replace_text, last_match_length)
660
661 replace_count = replace_count + 1
662
663 ! Guard against infinite loop
664 if (replace_count > 10000) exit
665 end do
666
667 ! Update main cursor
668 editor%cursors(editor%active_cursor) = temp_cursor
669 end subroutine replace_all_matches
670
671 subroutine perform_replacement(buffer, cursor, replace_text, match_len)
672 type(buffer_t), intent(inout) :: buffer
673 type(cursor_t), intent(inout) :: cursor
674 character(len=*), intent(in) :: replace_text
675 integer, intent(in) :: match_len
676 character(len=:), allocatable :: line, new_line
677 integer :: col, i
678
679 ! DEBUG: Log entry into perform_replacement
680 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
681 write(99, '(A,I0,A,I0,A,I0)') 'perform_replacement: cursor at line=', cursor%line, &
682 ' selection_start=', cursor%selection_start_line, ' col=', cursor%selection_start_col
683 close(99)
684
685 ! Get current line
686 line = buffer_get_line(buffer, cursor%line)
687
688 ! DEBUG: Log what line we got
689 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
690 write(99, '(A,A)') 'perform_replacement: line content="', trim(line), '"'
691 close(99)
692
693 ! Build new line with replacement
694 col = cursor%selection_start_col
695 allocate(character(len=len(line) - match_len + len(replace_text)) :: new_line)
696
697 ! Copy part before match
698 if (col > 1) then
699 new_line(1:col-1) = line(1:col-1)
700 end if
701
702 ! Insert replacement text
703 if (len(replace_text) > 0) then
704 new_line(col:col+len(replace_text)-1) = replace_text
705 end if
706
707 ! Copy part after match
708 if (col + match_len <= len(line)) then
709 new_line(col+len(replace_text):) = line(col+match_len:)
710 end if
711
712 ! DEBUG: Log the new line
713 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
714 write(99, '(A,A)') 'perform_replacement: new_line="', trim(new_line), '"'
715 close(99)
716
717 ! DEBUG: Log before delete
718 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
719 write(99, '(A,I0,A,I0)') 'perform_replacement: About to delete at line=', cursor%line, ' col=1'
720 close(99)
721
722 ! Delete old line content
723 cursor%column = 1
724 do i = 1, len(line)
725 call buffer_delete_at_cursor(buffer, cursor)
726 end do
727
728 ! DEBUG: Log after delete
729 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
730 write(99, '(A)') 'perform_replacement: Deleted old content'
731 close(99)
732
733 ! Insert new line content
734 do i = 1, len(new_line)
735 call buffer_insert_char(buffer, cursor, new_line(i:i))
736 cursor%column = cursor%column + 1
737 end do
738
739 ! DEBUG: Log after insert
740 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
741 write(99, '(A)') 'perform_replacement: Inserted new content'
742 line = buffer_get_line(buffer, 4)
743 write(99, '(A,A)') 'perform_replacement: Line 4 is now="', trim(line), '"'
744 if (allocated(line)) deallocate(line)
745 line = buffer_get_line(buffer, 1)
746 write(99, '(A,A)') 'perform_replacement: Line 1 is now="', trim(line), '"'
747 if (allocated(line)) deallocate(line)
748 close(99)
749
750 ! Position cursor after replacement
751 cursor%column = col + len(replace_text)
752 cursor%desired_column = cursor%column
753
754 buffer%modified = .true.
755
756 if (allocated(line)) deallocate(line)
757 if (allocated(new_line)) deallocate(new_line)
758 end subroutine perform_replacement
759
760 subroutine buffer_delete_at_cursor(buffer, cursor)
761 type(buffer_t), intent(inout) :: buffer
762 type(cursor_t), intent(in) :: cursor
763 integer :: pos
764
765 pos = get_buffer_position(buffer, cursor%line, cursor%column)
766
767 ! DEBUG: Log the position being deleted
768 if (cursor%line == 4) then
769 open(unit=99, file='/tmp/fac_debug.txt', position='append', action='write')
770 write(99, '(A,I0,A,I0,A,I0)') 'buffer_delete_at_cursor: line=', cursor%line, &
771 ' col=', cursor%column, ' calculated pos=', pos
772 close(99)
773 end if
774
775 if (pos > 0 .and. pos <= get_buffer_content_size(buffer)) then
776 call buffer_delete(buffer, pos, 1)
777 end if
778 end subroutine buffer_delete_at_cursor
779
780 subroutine buffer_insert_char(buffer, cursor, ch)
781 type(buffer_t), intent(inout) :: buffer
782 type(cursor_t), intent(in) :: cursor
783 character, intent(in) :: ch
784 integer :: pos
785
786 pos = get_buffer_position(buffer, cursor%line, cursor%column)
787 call buffer_insert(buffer, pos, ch)
788 end subroutine buffer_insert_char
789
790 function get_buffer_position(buffer, line, column) result(pos)
791 type(buffer_t), intent(in) :: buffer
792 integer, intent(in) :: line, column
793 integer :: pos
794 integer :: current_line, i
795 character :: ch
796
797 pos = 1
798 current_line = 1
799
800 do i = 1, get_buffer_content_size(buffer)
801 if (current_line == line .and. pos == column) then
802 pos = i ! Set pos to buffer position before returning
803 return
804 end if
805
806 ch = buffer_get_char(buffer, i)
807 if (ch == char(10)) then
808 if (current_line == line) then
809 pos = i ! Set pos to buffer position before returning
810 return
811 end if
812 current_line = current_line + 1
813 pos = 1
814 else if (current_line == line) then
815 pos = pos + 1
816 end if
817 end do
818
819 if (current_line == line) then
820 pos = i
821 else
822 pos = get_buffer_content_size(buffer) + 1
823 end if
824 end function get_buffer_position
825
826 function get_buffer_content_size(buffer) result(size)
827 type(buffer_t), intent(in) :: buffer
828 integer :: size
829
830 size = buffer%size - (buffer%gap_end - buffer%gap_start)
831 end function get_buffer_content_size
832
833 ! Include all helper functions from search_prompt_module
834 ! (find_next_match, find_prev_match, count_all_matches, etc.)
835 ! For brevity, I'll add a note that these need to be copied over
836
837 subroutine clear_search_pattern()
838 if (allocated(current_search_pattern)) deallocate(current_search_pattern)
839 if (allocated(current_replace_text)) deallocate(current_replace_text)
840 if (allocated(last_search_pattern)) deallocate(last_search_pattern)
841 search_mode_active = .false.
842 last_search_line = 1
843 last_search_col = 1
844
845 ! Reset search parameter tracking
846 last_case_sensitive = .false.
847 last_whole_word = .false.
848 last_use_regex = .false.
849
850 ! Free compiled regex if any
851 if (compiled_regex_id >= 0) then
852 call regex_free(compiled_regex_id)
853 compiled_regex_id = -1
854 end if
855 last_match_length = 0
856 end subroutine clear_search_pattern
857
858 ! Search helper functions
859 ! Check if a position is within the search selection bounds
860 function is_in_selection_bounds(line_num, col_num) result(in_bounds)
861 integer, intent(in) :: line_num, col_num
862 logical :: in_bounds
863
864 if (.not. search_in_selection) then
865 in_bounds = .true.
866 return
867 end if
868
869 ! Check if position is within selection
870 if (line_num < selection_start_line .or. line_num > selection_end_line) then
871 in_bounds = .false.
872 else if (line_num == selection_start_line .and. col_num < selection_start_col) then
873 in_bounds = .false.
874 else if (line_num == selection_end_line .and. col_num > selection_end_col) then
875 in_bounds = .false.
876 else
877 in_bounds = .true.
878 end if
879 end function is_in_selection_bounds
880
881 subroutine find_next_match(buffer, pattern, start_line, start_col, found, found_line, found_col)
882 type(buffer_t), intent(in) :: buffer
883 character(len=*), intent(in) :: pattern
884 integer, intent(in) :: start_line, start_col
885 logical, intent(out) :: found
886 integer, intent(out) :: found_line, found_col
887 character(len=:), allocatable :: line
888 integer :: line_count, current_line, pos, search_col
889 integer :: match_len
890 integer :: end_line
891
892 found = .false.
893 found_line = 0
894 found_col = 0
895 line_count = buffer_get_line_count(buffer)
896 last_match_length = 0
897
898 ! Determine search range
899 if (search_in_selection) then
900 end_line = selection_end_line
901 else
902 end_line = line_count
903 end if
904
905 ! Search from current position to end (of selection or file)
906 do current_line = start_line, end_line
907 line = buffer_get_line(buffer, current_line)
908 if (current_line == start_line) then
909 search_col = start_col + 1
910 else
911 search_col = 1
912 end if
913 if (search_col <= len(line)) then
914 if (use_regex) then
915 call find_regex_in_line(line(search_col:), compiled_regex_id, pos, match_len)
916 else
917 call find_pattern_in_line(line(search_col:), pattern, pos)
918 match_len = len(pattern)
919 end if
920 if (pos > 0) then
921 found_col = search_col + pos - 1
922 ! Check if match is within selection bounds
923 if (search_in_selection .and. .not. is_in_selection_bounds(current_line, found_col)) then
924 if (allocated(line)) deallocate(line)
925 cycle
926 end if
927 if (whole_word) then
928 if (.not. is_whole_word_match(line, found_col, match_len)) then
929 if (allocated(line)) deallocate(line)
930 cycle
931 end if
932 end if
933 found = .true.
934 found_line = current_line
935 last_match_length = match_len
936 if (allocated(line)) deallocate(line)
937 call update_match_index(buffer, pattern, found_line, found_col)
938 return
939 end if
940 end if
941 if (allocated(line)) deallocate(line)
942 end do
943
944 ! Wrap around to beginning (skip if searching in selection)
945 if (search_in_selection) return
946
947 do current_line = 1, start_line
948 line = buffer_get_line(buffer, current_line)
949 if (current_line == start_line) then
950 if (start_col > 1) then
951 if (use_regex) then
952 call find_regex_in_line(line(1:start_col-1), compiled_regex_id, pos, match_len)
953 else
954 call find_pattern_in_line(line(1:start_col-1), pattern, pos)
955 match_len = len(pattern)
956 end if
957 else
958 pos = 0
959 match_len = 0
960 end if
961 else
962 if (use_regex) then
963 call find_regex_in_line(line, compiled_regex_id, pos, match_len)
964 else
965 call find_pattern_in_line(line, pattern, pos)
966 match_len = len(pattern)
967 end if
968 end if
969 if (pos > 0) then
970 if (whole_word) then
971 if (.not. is_whole_word_match(line, pos, match_len)) then
972 if (allocated(line)) deallocate(line)
973 cycle
974 end if
975 end if
976 found = .true.
977 found_line = current_line
978 found_col = pos
979 last_match_length = match_len
980 if (allocated(line)) deallocate(line)
981 call update_match_index(buffer, pattern, found_line, found_col)
982 return
983 end if
984 if (allocated(line)) deallocate(line)
985 end do
986 end subroutine find_next_match
987
988 subroutine find_prev_match(buffer, pattern, start_line, start_col, found, found_line, found_col)
989 type(buffer_t), intent(in) :: buffer
990 character(len=*), intent(in) :: pattern
991 integer, intent(in) :: start_line, start_col
992 logical, intent(out) :: found
993 integer, intent(out) :: found_line, found_col
994 character(len=:), allocatable :: line
995 integer :: line_count, current_line, pos, last_pos, check_col
996 integer :: match_len, last_match_len
997 integer :: begin_line
998
999 found = .false.
1000 found_line = 0
1001 found_col = 0
1002 line_count = buffer_get_line_count(buffer)
1003 last_match_length = 0
1004
1005 ! Determine search range
1006 if (search_in_selection) then
1007 begin_line = selection_start_line
1008 else
1009 begin_line = 1
1010 end if
1011
1012 ! Search backward from current position
1013 do current_line = start_line, begin_line, -1
1014 line = buffer_get_line(buffer, current_line)
1015 if (current_line == start_line) then
1016 check_col = min(start_col, len(line))
1017 else
1018 check_col = len(line)
1019 end if
1020
1021 last_pos = 0
1022 last_match_len = 0
1023 pos = 1
1024 do while (pos <= check_col)
1025 if (use_regex) then
1026 call find_regex_in_line(line(pos:), compiled_regex_id, found_col, match_len)
1027 else
1028 call find_pattern_in_line(line(pos:), pattern, found_col)
1029 match_len = len(pattern)
1030 end if
1031 if (found_col > 0 .and. pos + found_col - 1 <= check_col) then
1032 if (whole_word) then
1033 if (is_whole_word_match(line, pos + found_col - 1, match_len)) then
1034 last_pos = pos + found_col - 1
1035 last_match_len = match_len
1036 end if
1037 else
1038 last_pos = pos + found_col - 1
1039 last_match_len = match_len
1040 end if
1041 pos = pos + found_col
1042 else
1043 exit
1044 end if
1045 end do
1046
1047 if (last_pos > 0) then
1048 ! Check if match is within selection bounds
1049 if (search_in_selection .and. .not. is_in_selection_bounds(current_line, last_pos)) then
1050 if (allocated(line)) deallocate(line)
1051 cycle
1052 end if
1053 found = .true.
1054 found_line = current_line
1055 found_col = last_pos
1056 last_match_length = last_match_len
1057 if (allocated(line)) deallocate(line)
1058 call update_match_index(buffer, pattern, found_line, found_col)
1059 return
1060 end if
1061 if (allocated(line)) deallocate(line)
1062 end do
1063 end subroutine find_prev_match
1064
1065 subroutine find_pattern_in_line(line, pattern, pos)
1066 character(len=*), intent(in) :: line
1067 character(len=*), intent(in) :: pattern
1068 integer, intent(out) :: pos
1069 character(len=:), allocatable :: search_line, search_pattern
1070 integer :: i
1071
1072 pos = 0
1073 if (len(line) == 0 .or. len(pattern) == 0) return
1074 if (len(pattern) > len(line)) return
1075
1076 if (case_sensitive) then
1077 pos = index(line, pattern)
1078 else
1079 allocate(character(len=len(line)) :: search_line)
1080 allocate(character(len=len(pattern)) :: search_pattern)
1081 do i = 1, len(line)
1082 if (iachar(line(i:i)) >= iachar('A') .and. iachar(line(i:i)) <= iachar('Z')) then
1083 search_line(i:i) = char(iachar(line(i:i)) + 32)
1084 else
1085 search_line(i:i) = line(i:i)
1086 end if
1087 end do
1088 do i = 1, len(pattern)
1089 if (iachar(pattern(i:i)) >= iachar('A') .and. iachar(pattern(i:i)) <= iachar('Z')) then
1090 search_pattern(i:i) = char(iachar(pattern(i:i)) + 32)
1091 else
1092 search_pattern(i:i) = pattern(i:i)
1093 end if
1094 end do
1095 pos = index(search_line, search_pattern)
1096 deallocate(search_line)
1097 deallocate(search_pattern)
1098 end if
1099 end subroutine find_pattern_in_line
1100
1101 ! Find pattern using regex in a line
1102 ! Returns position where match starts (1-based), or 0 if not found
1103 ! Also returns match_len (length of what was matched)
1104 subroutine find_regex_in_line(line, regex_id, pos, match_len)
1105 character(len=*), intent(in) :: line
1106 integer, intent(in) :: regex_id
1107 integer, intent(out) :: pos, match_len
1108 logical :: found
1109 integer :: start_pos, len_matched
1110
1111 if (regex_id < 0) then
1112 pos = 0
1113 match_len = 0
1114 return
1115 end if
1116
1117 found = regex_match(regex_id, line, start_pos, len_matched)
1118 if (found) then
1119 pos = start_pos
1120 match_len = len_matched
1121 else
1122 pos = 0
1123 match_len = 0
1124 end if
1125 end subroutine find_regex_in_line
1126
1127 logical function is_whole_word_match(line, start_pos, pattern_len)
1128 character(len=*), intent(in) :: line
1129 integer, intent(in) :: start_pos, pattern_len
1130 logical :: word_start, word_end
1131 integer :: end_pos
1132
1133 end_pos = start_pos + pattern_len - 1
1134 if (start_pos == 1) then
1135 word_start = .true.
1136 else
1137 word_start = .not. is_word_char(line(start_pos-1:start_pos-1))
1138 end if
1139 if (end_pos == len(line)) then
1140 word_end = .true.
1141 else
1142 word_end = .not. is_word_char(line(end_pos+1:end_pos+1))
1143 end if
1144 is_whole_word_match = word_start .and. word_end
1145 end function is_whole_word_match
1146
1147 logical function is_word_char(ch)
1148 character(len=1), intent(in) :: ch
1149 integer :: ascii_val
1150
1151 ascii_val = iachar(ch)
1152 is_word_char = (ascii_val >= iachar('A') .and. ascii_val <= iachar('Z')) .or. &
1153 (ascii_val >= iachar('a') .and. ascii_val <= iachar('z')) .or. &
1154 (ascii_val >= iachar('0') .and. ascii_val <= iachar('9')) .or. &
1155 (ch == '_')
1156 end function is_word_char
1157
1158 subroutine count_all_matches(buffer, pattern)
1159 type(buffer_t), intent(in) :: buffer
1160 character(len=*), intent(in) :: pattern
1161 character(len=:), allocatable :: line
1162 integer :: line_count, current_line, pos, col, match_count
1163 integer :: match_len
1164 integer :: start_line, end_line
1165
1166 match_count = 0
1167 line_count = buffer_get_line_count(buffer)
1168
1169 ! Determine search range
1170 if (search_in_selection) then
1171 start_line = selection_start_line
1172 end_line = selection_end_line
1173 else
1174 start_line = 1
1175 end_line = line_count
1176 end if
1177
1178 do current_line = start_line, end_line
1179 line = buffer_get_line(buffer, current_line)
1180 col = 1
1181 do while (col <= len(line))
1182 if (use_regex) then
1183 call find_regex_in_line(line(col:), compiled_regex_id, pos, match_len)
1184 else
1185 call find_pattern_in_line(line(col:), pattern, pos)
1186 match_len = len(pattern)
1187 end if
1188 if (pos > 0) then
1189 pos = col + pos - 1
1190 ! Check if match is within selection bounds
1191 if (search_in_selection .and. .not. is_in_selection_bounds(current_line, pos)) then
1192 col = pos + 1
1193 cycle
1194 end if
1195 if (whole_word) then
1196 if (is_whole_word_match(line, pos, match_len)) then
1197 match_count = match_count + 1
1198 end if
1199 else
1200 match_count = match_count + 1
1201 end if
1202 col = pos + 1
1203 else
1204 exit
1205 end if
1206 end do
1207 if (allocated(line)) deallocate(line)
1208 end do
1209
1210 total_matches = match_count
1211 current_match_index = 0
1212 end subroutine count_all_matches
1213
1214 subroutine update_match_index(buffer, pattern, match_line, match_col)
1215 type(buffer_t), intent(in) :: buffer
1216 character(len=*), intent(in) :: pattern
1217 integer, intent(in) :: match_line, match_col
1218 character(len=:), allocatable :: line
1219 integer :: line_count, current_line, pos, col, match_count
1220 integer :: match_len
1221 integer :: start_line, end_line
1222
1223 match_count = 0
1224 line_count = buffer_get_line_count(buffer)
1225
1226 ! Determine search range
1227 if (search_in_selection) then
1228 start_line = selection_start_line
1229 end_line = selection_end_line
1230 else
1231 start_line = 1
1232 end_line = line_count
1233 end if
1234
1235 do current_line = start_line, end_line
1236 line = buffer_get_line(buffer, current_line)
1237 col = 1
1238 do while (col <= len(line))
1239 if (use_regex) then
1240 call find_regex_in_line(line(col:), compiled_regex_id, pos, match_len)
1241 else
1242 call find_pattern_in_line(line(col:), pattern, pos)
1243 match_len = len(pattern)
1244 end if
1245 if (pos > 0) then
1246 pos = col + pos - 1
1247 ! Check if match is within selection bounds
1248 if (search_in_selection .and. .not. is_in_selection_bounds(current_line, pos)) then
1249 col = pos + 1
1250 cycle
1251 end if
1252 if (whole_word) then
1253 if (is_whole_word_match(line, pos, match_len)) then
1254 match_count = match_count + 1
1255 if (current_line == match_line .and. pos == match_col) then
1256 current_match_index = match_count
1257 if (allocated(line)) deallocate(line)
1258 return
1259 end if
1260 end if
1261 else
1262 match_count = match_count + 1
1263 if (current_line == match_line .and. pos == match_col) then
1264 current_match_index = match_count
1265 if (allocated(line)) deallocate(line)
1266 return
1267 end if
1268 end if
1269 col = pos + 1
1270 else
1271 exit
1272 end if
1273 end do
1274 if (allocated(line)) deallocate(line)
1275 end do
1276 end subroutine update_match_index
1277
1278 subroutine center_viewport_on_cursor(editor)
1279 type(editor_state_t), intent(inout) :: editor
1280 integer :: cursor_line, viewport_height
1281
1282 cursor_line = editor%cursors(editor%active_cursor)%line
1283 viewport_height = editor%screen_rows - 2
1284 editor%viewport_line = max(1, cursor_line - viewport_height / 2)
1285 end subroutine center_viewport_on_cursor
1286
1287 ! Add pattern to search history (avoiding duplicates)
1288 subroutine add_to_search_history(pattern)
1289 character(len=*), intent(in) :: pattern
1290 integer :: i
1291
1292 if (len_trim(pattern) == 0) return
1293
1294 ! Check if already in history (at position 1 = most recent)
1295 if (history_count > 0) then
1296 if (trim(search_history(1)) == trim(pattern)) return
1297 end if
1298
1299 ! Shift existing history down
1300 do i = min(history_count, MAX_HISTORY - 1), 1, -1
1301 search_history(i + 1) = search_history(i)
1302 end do
1303
1304 ! Add new pattern at top
1305 search_history(1) = pattern
1306 history_count = min(history_count + 1, MAX_HISTORY)
1307 history_index = 0 ! Reset navigation position
1308 end subroutine add_to_search_history
1309
1310 ! Navigate history up (to older entries)
1311 subroutine navigate_history_up(buffer, pos)
1312 character(len=*), intent(inout) :: buffer
1313 integer, intent(inout) :: pos
1314
1315 if (history_count == 0) return
1316
1317 ! Move to next older entry
1318 if (history_index < history_count) then
1319 history_index = history_index + 1
1320 buffer = search_history(history_index)
1321 pos = len_trim(buffer)
1322 end if
1323 end subroutine navigate_history_up
1324
1325 ! Navigate history down (to newer entries)
1326 subroutine navigate_history_down(buffer, pos)
1327 character(len=*), intent(inout) :: buffer
1328 integer, intent(inout) :: pos
1329
1330 if (history_count == 0) return
1331
1332 if (history_index > 1) then
1333 ! Move to next newer entry
1334 history_index = history_index - 1
1335 buffer = search_history(history_index)
1336 pos = len_trim(buffer)
1337 else if (history_index == 1) then
1338 ! Clear to allow new search
1339 history_index = 0
1340 buffer = ''
1341 pos = 0
1342 end if
1343 end subroutine navigate_history_down
1344
1345 ! Get all match positions on a given line
1346 ! Returns pairs of (start_col, end_col) for each match
1347 subroutine get_matches_on_line(line, line_num, matches, num_matches)
1348 character(len=*), intent(in) :: line
1349 integer, intent(in) :: line_num
1350 integer, intent(out) :: matches(:,:) ! Array of (start, end) pairs
1351 integer, intent(out) :: num_matches
1352 integer :: col, pos, match_len
1353 integer :: max_matches
1354
1355 num_matches = 0
1356 max_matches = size(matches, 2) ! Second dimension is number of match slots
1357
1358 ! Only highlight if search is active and we have a pattern
1359 if (.not. search_mode_active .or. .not. allocated(current_search_pattern)) return
1360 if (len_trim(current_search_pattern) == 0) return
1361
1362 ! Find all matches on this line
1363 col = 1
1364 do while (col <= len(line) .and. num_matches < max_matches)
1365 if (use_regex) then
1366 call find_regex_in_line(line(col:), compiled_regex_id, pos, match_len)
1367 else
1368 call find_pattern_in_line(line(col:), current_search_pattern, pos)
1369 match_len = len(current_search_pattern)
1370 end if
1371
1372 if (pos > 0) then
1373 pos = col + pos - 1 ! Adjust to line position
1374
1375 ! Check bounds and whole word if needed
1376 if (search_in_selection .and. .not. is_in_selection_bounds(line_num, pos)) then
1377 col = pos + 1
1378 cycle
1379 end if
1380 if (whole_word .and. .not. is_whole_word_match(line, pos, match_len)) then
1381 col = pos + 1
1382 cycle
1383 end if
1384
1385 ! Add match
1386 num_matches = num_matches + 1
1387 matches(1, num_matches) = pos
1388 matches(2, num_matches) = pos + match_len - 1
1389 col = pos + 1
1390 else
1391 exit
1392 end if
1393 end do
1394 end subroutine get_matches_on_line
1395
1396 end module unified_search_module
1397