| 1 | module references_panel_module |
| 2 | use iso_fortran_env, only: int32 |
| 3 | use terminal_io_module, only: terminal_move_cursor, terminal_write |
| 4 | implicit none |
| 5 | private |
| 6 | |
| 7 | public :: references_panel_t, reference_location_t |
| 8 | public :: init_references_panel, cleanup_references_panel |
| 9 | public :: show_references_panel, hide_references_panel, toggle_references_panel |
| 10 | public :: is_references_panel_visible, references_panel_handle_key |
| 11 | public :: set_references, clear_references |
| 12 | public :: get_selected_reference_location |
| 13 | public :: render_references_panel |
| 14 | |
| 15 | ! Reference location |
| 16 | type :: reference_location_t |
| 17 | character(len=:), allocatable :: uri |
| 18 | character(len=:), allocatable :: filename ! Extracted from URI |
| 19 | integer(int32) :: line |
| 20 | integer(int32) :: column |
| 21 | integer(int32) :: end_line |
| 22 | integer(int32) :: end_column |
| 23 | character(len=:), allocatable :: preview_text ! Line content for preview |
| 24 | end type reference_location_t |
| 25 | |
| 26 | ! References panel |
| 27 | type :: references_panel_t |
| 28 | logical :: visible = .false. |
| 29 | integer :: width = 50 ! Panel width in columns |
| 30 | integer :: selected_index = 1 |
| 31 | integer :: scroll_offset = 0 |
| 32 | |
| 33 | ! References data |
| 34 | type(reference_location_t), allocatable :: references(:) |
| 35 | integer :: num_references = 0 |
| 36 | character(len=:), allocatable :: symbol_name |
| 37 | |
| 38 | ! Screen position |
| 39 | integer :: screen_width = 80 |
| 40 | integer :: screen_height = 24 |
| 41 | end type references_panel_t |
| 42 | |
| 43 | contains |
| 44 | |
| 45 | subroutine init_references_panel(panel) |
| 46 | type(references_panel_t), intent(out) :: panel |
| 47 | |
| 48 | panel%visible = .false. |
| 49 | panel%width = 50 |
| 50 | panel%selected_index = 1 |
| 51 | panel%scroll_offset = 0 |
| 52 | panel%num_references = 0 |
| 53 | panel%screen_width = 80 |
| 54 | panel%screen_height = 24 |
| 55 | end subroutine init_references_panel |
| 56 | |
| 57 | subroutine cleanup_references_panel(panel) |
| 58 | type(references_panel_t), intent(inout) :: panel |
| 59 | integer :: i |
| 60 | |
| 61 | if (allocated(panel%references)) then |
| 62 | do i = 1, panel%num_references |
| 63 | if (allocated(panel%references(i)%uri)) deallocate(panel%references(i)%uri) |
| 64 | if (allocated(panel%references(i)%filename)) deallocate(panel%references(i)%filename) |
| 65 | if (allocated(panel%references(i)%preview_text)) deallocate(panel%references(i)%preview_text) |
| 66 | end do |
| 67 | deallocate(panel%references) |
| 68 | end if |
| 69 | |
| 70 | if (allocated(panel%symbol_name)) deallocate(panel%symbol_name) |
| 71 | |
| 72 | panel%num_references = 0 |
| 73 | panel%selected_index = 1 |
| 74 | panel%scroll_offset = 0 |
| 75 | end subroutine cleanup_references_panel |
| 76 | |
| 77 | subroutine set_references(panel, references, num_refs, symbol_name) |
| 78 | type(references_panel_t), intent(inout) :: panel |
| 79 | type(reference_location_t), intent(in) :: references(:) |
| 80 | integer, intent(in) :: num_refs |
| 81 | character(len=*), intent(in), optional :: symbol_name |
| 82 | integer :: i |
| 83 | |
| 84 | ! Clear existing references |
| 85 | call cleanup_references_panel(panel) |
| 86 | |
| 87 | ! Allocate and copy new references |
| 88 | if (num_refs > 0) then |
| 89 | allocate(panel%references(num_refs)) |
| 90 | panel%num_references = num_refs |
| 91 | |
| 92 | do i = 1, num_refs |
| 93 | if (allocated(references(i)%uri)) then |
| 94 | allocate(character(len=len(references(i)%uri)) :: panel%references(i)%uri) |
| 95 | panel%references(i)%uri = references(i)%uri |
| 96 | end if |
| 97 | |
| 98 | if (allocated(references(i)%filename)) then |
| 99 | allocate(character(len=len(references(i)%filename)) :: panel%references(i)%filename) |
| 100 | panel%references(i)%filename = references(i)%filename |
| 101 | end if |
| 102 | |
| 103 | if (allocated(references(i)%preview_text)) then |
| 104 | allocate(character(len=len(references(i)%preview_text)) :: panel%references(i)%preview_text) |
| 105 | panel%references(i)%preview_text = references(i)%preview_text |
| 106 | end if |
| 107 | |
| 108 | panel%references(i)%line = references(i)%line |
| 109 | panel%references(i)%column = references(i)%column |
| 110 | panel%references(i)%end_line = references(i)%end_line |
| 111 | panel%references(i)%end_column = references(i)%end_column |
| 112 | end do |
| 113 | end if |
| 114 | |
| 115 | ! Set symbol name |
| 116 | if (present(symbol_name)) then |
| 117 | if (allocated(panel%symbol_name)) deallocate(panel%symbol_name) |
| 118 | allocate(character(len=len_trim(symbol_name)) :: panel%symbol_name) |
| 119 | panel%symbol_name = trim(symbol_name) |
| 120 | end if |
| 121 | |
| 122 | panel%selected_index = 1 |
| 123 | panel%scroll_offset = 0 |
| 124 | end subroutine set_references |
| 125 | |
| 126 | subroutine clear_references(panel) |
| 127 | type(references_panel_t), intent(inout) :: panel |
| 128 | call cleanup_references_panel(panel) |
| 129 | end subroutine clear_references |
| 130 | |
| 131 | subroutine show_references_panel(panel, screen_width, screen_height) |
| 132 | type(references_panel_t), intent(inout) :: panel |
| 133 | integer, intent(in) :: screen_width, screen_height |
| 134 | |
| 135 | panel%screen_width = screen_width |
| 136 | panel%screen_height = screen_height |
| 137 | panel%visible = .true. |
| 138 | end subroutine show_references_panel |
| 139 | |
| 140 | subroutine hide_references_panel(panel) |
| 141 | type(references_panel_t), intent(inout) :: panel |
| 142 | panel%visible = .false. |
| 143 | end subroutine hide_references_panel |
| 144 | |
| 145 | subroutine toggle_references_panel(panel) |
| 146 | type(references_panel_t), intent(inout) :: panel |
| 147 | panel%visible = .not. panel%visible |
| 148 | end subroutine toggle_references_panel |
| 149 | |
| 150 | function is_references_panel_visible(panel) result(visible) |
| 151 | type(references_panel_t), intent(in) :: panel |
| 152 | logical :: visible |
| 153 | visible = panel%visible |
| 154 | end function is_references_panel_visible |
| 155 | |
| 156 | subroutine render_references_panel(panel, start_row) |
| 157 | type(references_panel_t), intent(in) :: panel |
| 158 | integer, intent(in) :: start_row |
| 159 | integer :: row, col, start_col |
| 160 | integer :: i, visible_index, max_visible |
| 161 | character(len=256) :: line |
| 162 | character(len=100) :: header, location_str |
| 163 | character(len=:), allocatable :: display_text |
| 164 | |
| 165 | if (.not. panel%visible) return |
| 166 | |
| 167 | ! Calculate panel position (right side of screen) |
| 168 | start_col = panel%screen_width - panel%width + 1 |
| 169 | max_visible = panel%screen_height - start_row - 2 ! Leave room for header/footer |
| 170 | |
| 171 | ! Draw panel border and header |
| 172 | row = start_row |
| 173 | |
| 174 | ! Header with symbol name |
| 175 | call terminal_move_cursor(row, start_col) |
| 176 | call terminal_write(char(27) // '[48;5;237m') ! Dark background |
| 177 | |
| 178 | if (allocated(panel%symbol_name)) then |
| 179 | write(header, '(A,A,A,I0,A)') " References: ", trim(panel%symbol_name), & |
| 180 | " (", panel%num_references, ") " |
| 181 | else |
| 182 | write(header, '(A,I0,A)') " References (", panel%num_references, ") " |
| 183 | end if |
| 184 | |
| 185 | ! Truncate header if too long |
| 186 | if (len_trim(header) > panel%width) then |
| 187 | header = header(1:panel%width-3) // "..." |
| 188 | end if |
| 189 | |
| 190 | ! Center the header |
| 191 | col = start_col + (panel%width - len_trim(header)) / 2 |
| 192 | call terminal_move_cursor(row, col) |
| 193 | call terminal_write(char(27) // '[1m' // trim(header) // char(27) // '[0m') |
| 194 | |
| 195 | ! Clear to end of header line |
| 196 | call terminal_move_cursor(row, start_col + len_trim(header)) |
| 197 | call render_empty_line(panel%width - len_trim(header)) |
| 198 | |
| 199 | row = row + 1 |
| 200 | |
| 201 | ! Draw separator |
| 202 | call terminal_move_cursor(row, start_col) |
| 203 | call terminal_write(char(27) // '[48;5;237m' // repeat("─", panel%width) // char(27) // '[0m') |
| 204 | row = row + 1 |
| 205 | |
| 206 | ! Display references |
| 207 | if (panel%num_references == 0) then |
| 208 | call terminal_move_cursor(row, start_col) |
| 209 | call terminal_write(char(27) // '[48;5;235m' // char(27) // '[90m') |
| 210 | call terminal_write(" No references found") |
| 211 | call terminal_write(char(27) // '[K') ! Clear to end of line |
| 212 | call terminal_write(char(27) // '[0m') |
| 213 | else |
| 214 | ! Display visible references |
| 215 | do i = 1, min(max_visible, panel%num_references - panel%scroll_offset) |
| 216 | visible_index = panel%scroll_offset + i |
| 217 | |
| 218 | if (visible_index > panel%num_references) exit |
| 219 | |
| 220 | call terminal_move_cursor(row, start_col) |
| 221 | |
| 222 | ! Highlight selected item |
| 223 | if (visible_index == panel%selected_index) then |
| 224 | call terminal_write(char(27) // '[48;5;240m') ! Highlight background |
| 225 | else |
| 226 | call terminal_write(char(27) // '[48;5;235m') ! Normal background |
| 227 | end if |
| 228 | |
| 229 | ! Format location string |
| 230 | if (allocated(panel%references(visible_index)%filename)) then |
| 231 | write(location_str, '(A,A,I0,A,I0)') & |
| 232 | trim(get_basename(panel%references(visible_index)%filename)), & |
| 233 | ":", panel%references(visible_index)%line, & |
| 234 | ":", panel%references(visible_index)%column |
| 235 | else |
| 236 | write(location_str, '(I0,A,I0)') & |
| 237 | panel%references(visible_index)%line, & |
| 238 | ":", panel%references(visible_index)%column |
| 239 | end if |
| 240 | |
| 241 | ! Truncate location if needed |
| 242 | if (len_trim(location_str) > 20) then |
| 243 | location_str = location_str(1:17) // "..." |
| 244 | end if |
| 245 | |
| 246 | ! Format display line |
| 247 | write(line, '(A2,A20,A)') " ", adjustl(location_str), " " |
| 248 | |
| 249 | ! Add preview text if available |
| 250 | if (allocated(panel%references(visible_index)%preview_text)) then |
| 251 | display_text = trim(panel%references(visible_index)%preview_text) |
| 252 | if (len(display_text) > panel%width - 25) then |
| 253 | display_text = display_text(1:panel%width-28) // "..." |
| 254 | end if |
| 255 | line = trim(line) // display_text |
| 256 | end if |
| 257 | |
| 258 | ! Ensure line fits in panel width |
| 259 | if (len_trim(line) > panel%width) then |
| 260 | line = line(1:panel%width) |
| 261 | end if |
| 262 | |
| 263 | call terminal_write(line(1:panel%width)) |
| 264 | call terminal_write(char(27) // '[0m') |
| 265 | |
| 266 | row = row + 1 |
| 267 | end do |
| 268 | |
| 269 | ! Clear remaining lines |
| 270 | do i = row, start_row + max_visible + 1 |
| 271 | if (i > panel%screen_height - 1) exit |
| 272 | call terminal_move_cursor(i, start_col) |
| 273 | call render_empty_line(panel%width) |
| 274 | end do |
| 275 | |
| 276 | ! Show scroll indicator if needed |
| 277 | if (panel%num_references > max_visible) then |
| 278 | call terminal_move_cursor(start_row + max_visible + 2, start_col) |
| 279 | call terminal_write(char(27) // '[48;5;237m' // char(27) // '[90m') |
| 280 | write(line, '(A,I0,A,I0,A)') " [", panel%selected_index, "/", panel%num_references, "] " |
| 281 | if (panel%scroll_offset > 0) then |
| 282 | line = trim(line) // "↑" |
| 283 | end if |
| 284 | if (panel%scroll_offset + max_visible < panel%num_references) then |
| 285 | line = trim(line) // "↓" |
| 286 | end if |
| 287 | call terminal_write(trim(line)) |
| 288 | call terminal_write(char(27) // '[0m') |
| 289 | end if |
| 290 | end if |
| 291 | end subroutine render_references_panel |
| 292 | |
| 293 | subroutine render_empty_line(width) |
| 294 | integer, intent(in) :: width |
| 295 | call terminal_write(char(27) // '[48;5;235m' // repeat(" ", width) // char(27) // '[0m') |
| 296 | end subroutine render_empty_line |
| 297 | |
| 298 | function references_panel_handle_key(panel, key) result(handled) |
| 299 | type(references_panel_t), intent(inout) :: panel |
| 300 | character(len=*), intent(in) :: key |
| 301 | logical :: handled |
| 302 | integer :: max_visible |
| 303 | |
| 304 | handled = .false. |
| 305 | if (.not. panel%visible) return |
| 306 | |
| 307 | max_visible = panel%screen_height - 4 |
| 308 | |
| 309 | select case(trim(key)) |
| 310 | case('j', 'down') |
| 311 | ! Move selection down |
| 312 | if (panel%selected_index < panel%num_references) then |
| 313 | panel%selected_index = panel%selected_index + 1 |
| 314 | |
| 315 | ! Adjust scroll if needed |
| 316 | if (panel%selected_index > panel%scroll_offset + max_visible) then |
| 317 | panel%scroll_offset = panel%selected_index - max_visible |
| 318 | end if |
| 319 | handled = .true. |
| 320 | end if |
| 321 | |
| 322 | case('k', 'up') |
| 323 | ! Move selection up |
| 324 | if (panel%selected_index > 1) then |
| 325 | panel%selected_index = panel%selected_index - 1 |
| 326 | |
| 327 | ! Adjust scroll if needed |
| 328 | if (panel%selected_index <= panel%scroll_offset) then |
| 329 | panel%scroll_offset = max(0, panel%selected_index - 1) |
| 330 | end if |
| 331 | handled = .true. |
| 332 | end if |
| 333 | |
| 334 | case('pagedown') |
| 335 | ! Page down |
| 336 | panel%selected_index = min(panel%num_references, & |
| 337 | panel%selected_index + max_visible) |
| 338 | panel%scroll_offset = min(max(0, panel%num_references - max_visible), & |
| 339 | panel%scroll_offset + max_visible) |
| 340 | handled = .true. |
| 341 | |
| 342 | case('pageup') |
| 343 | ! Page up |
| 344 | panel%selected_index = max(1, panel%selected_index - max_visible) |
| 345 | panel%scroll_offset = max(0, panel%scroll_offset - max_visible) |
| 346 | handled = .true. |
| 347 | |
| 348 | case('home') |
| 349 | ! Jump to first |
| 350 | panel%selected_index = 1 |
| 351 | panel%scroll_offset = 0 |
| 352 | handled = .true. |
| 353 | |
| 354 | case('end') |
| 355 | ! Jump to last |
| 356 | panel%selected_index = panel%num_references |
| 357 | panel%scroll_offset = max(0, panel%num_references - max_visible) |
| 358 | handled = .true. |
| 359 | |
| 360 | case('enter') |
| 361 | ! User wants to jump to this reference |
| 362 | handled = .true. |
| 363 | |
| 364 | case('escape', 'shift-f12') |
| 365 | ! Hide panel |
| 366 | panel%visible = .false. |
| 367 | handled = .true. |
| 368 | end select |
| 369 | end function references_panel_handle_key |
| 370 | |
| 371 | function get_selected_reference_location(panel, uri, line, col) result(has_location) |
| 372 | type(references_panel_t), intent(in) :: panel |
| 373 | character(len=:), allocatable, intent(out) :: uri |
| 374 | integer(int32), intent(out) :: line, col |
| 375 | logical :: has_location |
| 376 | |
| 377 | has_location = .false. |
| 378 | |
| 379 | if (panel%selected_index > 0 .and. panel%selected_index <= panel%num_references) then |
| 380 | if (allocated(panel%references(panel%selected_index)%uri)) then |
| 381 | allocate(character(len=len(panel%references(panel%selected_index)%uri)) :: uri) |
| 382 | uri = panel%references(panel%selected_index)%uri |
| 383 | line = panel%references(panel%selected_index)%line |
| 384 | col = panel%references(panel%selected_index)%column |
| 385 | has_location = .true. |
| 386 | end if |
| 387 | end if |
| 388 | end function get_selected_reference_location |
| 389 | |
| 390 | ! Helper function to extract basename from path |
| 391 | function get_basename(path) result(basename) |
| 392 | character(len=*), intent(in) :: path |
| 393 | character(len=:), allocatable :: basename |
| 394 | integer :: last_slash |
| 395 | |
| 396 | last_slash = index(path, '/', back=.true.) |
| 397 | if (last_slash > 0) then |
| 398 | basename = path(last_slash+1:) |
| 399 | else |
| 400 | basename = path |
| 401 | end if |
| 402 | end function get_basename |
| 403 | |
| 404 | end module references_panel_module |