| 1 | module replace_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 | use search_prompt_module, only: find_next_match, center_viewport_on_cursor |
| 7 | use renderer_module, only: render_screen |
| 8 | implicit none |
| 9 | private |
| 10 | |
| 11 | public :: show_replace_prompt |
| 12 | |
| 13 | contains |
| 14 | |
| 15 | subroutine show_replace_prompt(editor, buffer) |
| 16 | type(editor_state_t), intent(inout) :: editor |
| 17 | type(buffer_t), intent(inout) :: buffer |
| 18 | character(len=256) :: find_buffer, replace_buffer |
| 19 | character(len=64) :: prompt |
| 20 | integer :: input_pos, ch |
| 21 | logical :: entering_find, entering_replace |
| 22 | integer :: replace_count |
| 23 | character(len=:), allocatable :: find_pattern, replace_text |
| 24 | |
| 25 | ! Initialize allocatables to prevent "may be uninitialized" warning |
| 26 | allocate(character(len=0) :: find_pattern) |
| 27 | allocate(character(len=0) :: replace_text) |
| 28 | |
| 29 | ! Initialize |
| 30 | find_buffer = '' |
| 31 | replace_buffer = '' |
| 32 | input_pos = 0 |
| 33 | entering_find = .true. |
| 34 | entering_replace = .false. |
| 35 | replace_count = 0 |
| 36 | |
| 37 | ! Display initial prompt |
| 38 | prompt = 'Replace: ' |
| 39 | call terminal_move_cursor(editor%screen_rows, 1) |
| 40 | call terminal_write(prompt) |
| 41 | call terminal_show_cursor() |
| 42 | |
| 43 | ! Input loop for find pattern |
| 44 | do |
| 45 | ch = terminal_read_char() |
| 46 | |
| 47 | if (ch == -1) then |
| 48 | cycle |
| 49 | else if (ch == 27) then ! ESC - cancel |
| 50 | exit |
| 51 | else if (ch == 10 .or. ch == 13) then ! Enter - proceed to replacement |
| 52 | if (input_pos > 0 .and. entering_find) then |
| 53 | allocate(character(len=input_pos) :: find_pattern) |
| 54 | find_pattern = find_buffer(1:input_pos) |
| 55 | entering_find = .false. |
| 56 | entering_replace = .true. |
| 57 | input_pos = 0 |
| 58 | |
| 59 | ! Update prompt for replacement |
| 60 | prompt = 'With: ' |
| 61 | call terminal_move_cursor(editor%screen_rows, 1) |
| 62 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 63 | call terminal_move_cursor(editor%screen_rows, 1) |
| 64 | call terminal_write(prompt) |
| 65 | else if (entering_replace) then |
| 66 | allocate(character(len=input_pos) :: replace_text) |
| 67 | if (input_pos > 0) then |
| 68 | replace_text = replace_buffer(1:input_pos) |
| 69 | else |
| 70 | replace_text = '' |
| 71 | end if |
| 72 | exit |
| 73 | end if |
| 74 | else if (ch == 127 .or. ch == 8) then ! Backspace |
| 75 | if (input_pos > 0) then |
| 76 | input_pos = input_pos - 1 |
| 77 | call terminal_move_cursor(editor%screen_rows, 1) |
| 78 | if (entering_find) then |
| 79 | call terminal_write(prompt // find_buffer(1:input_pos) // ' ') |
| 80 | else |
| 81 | call terminal_write(prompt // replace_buffer(1:input_pos) // ' ') |
| 82 | end if |
| 83 | call terminal_move_cursor(editor%screen_rows, len(prompt) + input_pos + 1) |
| 84 | end if |
| 85 | else if (ch >= 32 .and. ch <= 126) then ! Printable characters |
| 86 | if (input_pos < 256) then |
| 87 | input_pos = input_pos + 1 |
| 88 | if (entering_find) then |
| 89 | find_buffer(input_pos:input_pos) = char(ch) |
| 90 | else |
| 91 | replace_buffer(input_pos:input_pos) = char(ch) |
| 92 | end if |
| 93 | call terminal_write(char(ch)) |
| 94 | end if |
| 95 | end if |
| 96 | end do |
| 97 | |
| 98 | ! Clean up prompt line |
| 99 | call terminal_hide_cursor() |
| 100 | call terminal_move_cursor(editor%screen_rows, 1) |
| 101 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 102 | |
| 103 | ! If we have both patterns, show replace options |
| 104 | if (allocated(find_pattern) .and. allocated(replace_text)) then |
| 105 | call execute_replace(editor, buffer, find_pattern, replace_text, replace_count) |
| 106 | |
| 107 | ! Show result |
| 108 | call terminal_move_cursor(editor%screen_rows, 1) |
| 109 | if (replace_count > 0) then |
| 110 | write(prompt, '(a,i0,a)') 'Replaced ', replace_count, ' occurrences' |
| 111 | else |
| 112 | prompt = 'No matches found' |
| 113 | end if |
| 114 | call terminal_write(trim(prompt)) |
| 115 | call flush(output_unit) |
| 116 | |
| 117 | ! Brief pause to show message |
| 118 | call sleep_ms(1500) |
| 119 | |
| 120 | ! Clear message |
| 121 | call terminal_move_cursor(editor%screen_rows, 1) |
| 122 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 123 | end if |
| 124 | |
| 125 | ! Clean up |
| 126 | if (allocated(find_pattern)) deallocate(find_pattern) |
| 127 | if (allocated(replace_text)) deallocate(replace_text) |
| 128 | end subroutine show_replace_prompt |
| 129 | |
| 130 | subroutine execute_replace(editor, buffer, find_pattern, replace_text, replace_count) |
| 131 | type(editor_state_t), intent(inout) :: editor |
| 132 | type(buffer_t), intent(inout) :: buffer |
| 133 | character(len=*), intent(in) :: find_pattern, replace_text |
| 134 | integer, intent(out) :: replace_count |
| 135 | logical :: found |
| 136 | integer :: found_line, found_col |
| 137 | integer :: start_line, start_col |
| 138 | character(len=64) :: prompt |
| 139 | character :: response |
| 140 | integer :: ch |
| 141 | logical :: replace_all |
| 142 | |
| 143 | replace_count = 0 |
| 144 | replace_all = .false. |
| 145 | start_line = 1 |
| 146 | start_col = 1 |
| 147 | |
| 148 | ! Find first match |
| 149 | call find_next_match(buffer, find_pattern, start_line, start_col, & |
| 150 | found, found_line, found_col) |
| 151 | |
| 152 | do while (found) |
| 153 | ! Move cursor to match |
| 154 | editor%cursors(editor%active_cursor)%line = found_line |
| 155 | editor%cursors(editor%active_cursor)%column = found_col |
| 156 | editor%cursors(editor%active_cursor)%desired_column = found_col |
| 157 | |
| 158 | ! Select the match |
| 159 | editor%cursors(editor%active_cursor)%has_selection = .true. |
| 160 | editor%cursors(editor%active_cursor)%selection_start_line = found_line |
| 161 | editor%cursors(editor%active_cursor)%selection_start_col = found_col |
| 162 | editor%cursors(editor%active_cursor)%column = found_col + len(find_pattern) |
| 163 | |
| 164 | ! Update viewport and render |
| 165 | call center_viewport_on_cursor(editor) |
| 166 | call render_screen(buffer, editor) |
| 167 | |
| 168 | if (.not. replace_all) then |
| 169 | ! Ask for confirmation |
| 170 | call terminal_move_cursor(editor%screen_rows, 1) |
| 171 | prompt = 'Replace? (y/n/a/q): ' |
| 172 | call terminal_write(trim(prompt)) |
| 173 | call terminal_show_cursor() |
| 174 | |
| 175 | ! Get response |
| 176 | do |
| 177 | ch = terminal_read_char() |
| 178 | if (ch > 0) then |
| 179 | response = char(ch) |
| 180 | exit |
| 181 | end if |
| 182 | end do |
| 183 | |
| 184 | call terminal_hide_cursor() |
| 185 | call terminal_move_cursor(editor%screen_rows, 1) |
| 186 | call terminal_write(repeat(' ', editor%screen_cols)) |
| 187 | |
| 188 | select case(response) |
| 189 | case('y', 'Y') |
| 190 | ! Replace this occurrence |
| 191 | call perform_replacement(buffer, editor%cursors(editor%active_cursor), & |
| 192 | find_pattern, replace_text) |
| 193 | replace_count = replace_count + 1 |
| 194 | |
| 195 | case('n', 'N') |
| 196 | ! Skip this occurrence |
| 197 | |
| 198 | case('a', 'A') |
| 199 | ! Replace all remaining |
| 200 | replace_all = .true. |
| 201 | call perform_replacement(buffer, editor%cursors(editor%active_cursor), & |
| 202 | find_pattern, replace_text) |
| 203 | replace_count = replace_count + 1 |
| 204 | |
| 205 | case('q', 'Q', char(27)) ! q or ESC |
| 206 | ! Quit replacing |
| 207 | exit |
| 208 | |
| 209 | case default |
| 210 | ! Invalid response, skip |
| 211 | end select |
| 212 | else |
| 213 | ! Replace without asking |
| 214 | call perform_replacement(buffer, editor%cursors(editor%active_cursor), & |
| 215 | find_pattern, replace_text) |
| 216 | replace_count = replace_count + 1 |
| 217 | end if |
| 218 | |
| 219 | ! Clear selection |
| 220 | editor%cursors(editor%active_cursor)%has_selection = .false. |
| 221 | |
| 222 | ! Find next match (starting after the replacement) |
| 223 | start_line = editor%cursors(editor%active_cursor)%line |
| 224 | start_col = editor%cursors(editor%active_cursor)%column |
| 225 | |
| 226 | call find_next_match(buffer, find_pattern, start_line, start_col, & |
| 227 | found, found_line, found_col) |
| 228 | |
| 229 | ! Check if we've wrapped around to the beginning |
| 230 | if (found .and. found_line <= start_line .and. found_col < start_col) then |
| 231 | exit |
| 232 | end if |
| 233 | end do |
| 234 | end subroutine execute_replace |
| 235 | |
| 236 | subroutine perform_replacement(buffer, cursor, find_pattern, replace_text) |
| 237 | type(buffer_t), intent(inout) :: buffer |
| 238 | type(cursor_t), intent(inout) :: cursor |
| 239 | character(len=*), intent(in) :: find_pattern, replace_text |
| 240 | character(len=:), allocatable :: line, new_line |
| 241 | integer :: col, i |
| 242 | |
| 243 | ! Get current line |
| 244 | line = buffer_get_line(buffer, cursor%line) |
| 245 | |
| 246 | ! Build new line with replacement |
| 247 | col = cursor%selection_start_col |
| 248 | allocate(character(len=len(line) - len(find_pattern) + len(replace_text)) :: new_line) |
| 249 | |
| 250 | ! Copy part before match |
| 251 | if (col > 1) then |
| 252 | new_line(1:col-1) = line(1:col-1) |
| 253 | end if |
| 254 | |
| 255 | ! Insert replacement text |
| 256 | if (len(replace_text) > 0) then |
| 257 | new_line(col:col+len(replace_text)-1) = replace_text |
| 258 | end if |
| 259 | |
| 260 | ! Copy part after match |
| 261 | if (col + len(find_pattern) <= len(line)) then |
| 262 | new_line(col+len(replace_text):) = line(col+len(find_pattern):) |
| 263 | end if |
| 264 | |
| 265 | ! Delete old line content |
| 266 | cursor%column = 1 |
| 267 | do i = 1, len(line) |
| 268 | call buffer_delete_at_cursor(buffer, cursor) |
| 269 | end do |
| 270 | |
| 271 | ! Insert new line content |
| 272 | do i = 1, len(new_line) |
| 273 | call buffer_insert_char(buffer, cursor, new_line(i:i)) |
| 274 | cursor%column = cursor%column + 1 |
| 275 | end do |
| 276 | |
| 277 | ! Position cursor after replacement |
| 278 | cursor%column = col + len(replace_text) |
| 279 | cursor%desired_column = cursor%column |
| 280 | |
| 281 | buffer%modified = .true. |
| 282 | |
| 283 | if (allocated(line)) deallocate(line) |
| 284 | if (allocated(new_line)) deallocate(new_line) |
| 285 | end subroutine perform_replacement |
| 286 | |
| 287 | subroutine buffer_delete_at_cursor(buffer, cursor) |
| 288 | type(buffer_t), intent(inout) :: buffer |
| 289 | type(cursor_t), intent(in) :: cursor |
| 290 | integer :: pos |
| 291 | |
| 292 | pos = get_buffer_position(buffer, cursor%line, cursor%column) |
| 293 | if (pos > 0 .and. pos <= get_buffer_content_size(buffer)) then |
| 294 | call buffer_delete(buffer, pos, 1) |
| 295 | end if |
| 296 | end subroutine buffer_delete_at_cursor |
| 297 | |
| 298 | subroutine buffer_insert_char(buffer, cursor, ch) |
| 299 | type(buffer_t), intent(inout) :: buffer |
| 300 | type(cursor_t), intent(in) :: cursor |
| 301 | character, intent(in) :: ch |
| 302 | integer :: pos |
| 303 | |
| 304 | pos = get_buffer_position(buffer, cursor%line, cursor%column) |
| 305 | call buffer_insert(buffer, pos, ch) |
| 306 | end subroutine buffer_insert_char |
| 307 | |
| 308 | function get_buffer_position(buffer, line, column) result(pos) |
| 309 | type(buffer_t), intent(in) :: buffer |
| 310 | integer, intent(in) :: line, column |
| 311 | integer :: pos |
| 312 | integer :: current_line, i |
| 313 | character :: ch |
| 314 | |
| 315 | pos = 1 |
| 316 | current_line = 1 |
| 317 | |
| 318 | do i = 1, get_buffer_content_size(buffer) |
| 319 | if (current_line == line .and. pos == column) then |
| 320 | return |
| 321 | end if |
| 322 | |
| 323 | ch = buffer_get_char(buffer, i) |
| 324 | if (ch == char(10)) then |
| 325 | if (current_line == line) then |
| 326 | return |
| 327 | end if |
| 328 | current_line = current_line + 1 |
| 329 | pos = 1 |
| 330 | else if (current_line == line) then |
| 331 | pos = pos + 1 |
| 332 | end if |
| 333 | end do |
| 334 | |
| 335 | if (current_line == line) then |
| 336 | pos = i |
| 337 | else |
| 338 | pos = get_buffer_content_size(buffer) + 1 |
| 339 | end if |
| 340 | end function get_buffer_position |
| 341 | |
| 342 | function get_buffer_content_size(buffer) result(size) |
| 343 | type(buffer_t), intent(in) :: buffer |
| 344 | integer :: size |
| 345 | |
| 346 | size = buffer%size - (buffer%gap_end - buffer%gap_start) |
| 347 | end function get_buffer_content_size |
| 348 | |
| 349 | subroutine sleep_ms(milliseconds) |
| 350 | integer, intent(in) :: milliseconds |
| 351 | integer :: count_rate, count_start, count_end |
| 352 | real :: elapsed_time |
| 353 | |
| 354 | call system_clock(count_rate=count_rate) |
| 355 | call system_clock(count=count_start) |
| 356 | |
| 357 | do |
| 358 | call system_clock(count=count_end) |
| 359 | elapsed_time = real(count_end - count_start) / real(count_rate) * 1000.0 |
| 360 | if (elapsed_time >= milliseconds) exit |
| 361 | end do |
| 362 | end subroutine sleep_ms |
| 363 | |
| 364 | end module replace_prompt_module |