| 1 | module diagnostics_panel_module |
| 2 | use iso_fortran_env, only: int32 |
| 3 | use diagnostics_module, only: diagnostic_t, diagnostics_store_t, & |
| 4 | get_diagnostics_for_file, & |
| 5 | SEVERITY_ERROR, SEVERITY_WARNING, & |
| 6 | SEVERITY_INFO, SEVERITY_HINT |
| 7 | use terminal_io_module, only: terminal_write, terminal_move_cursor |
| 8 | implicit none |
| 9 | private |
| 10 | |
| 11 | public :: diagnostics_panel_t |
| 12 | public :: init_diagnostics_panel, cleanup_diagnostics_panel |
| 13 | public :: render_diagnostics_panel, toggle_diagnostics_panel |
| 14 | public :: diagnostics_panel_handle_key |
| 15 | public :: is_diagnostics_panel_visible |
| 16 | |
| 17 | type :: diagnostics_panel_t |
| 18 | logical :: visible = .false. |
| 19 | integer :: width = 40 ! Panel width in columns |
| 20 | integer :: selected_index = 1 ! Currently selected diagnostic |
| 21 | integer :: scroll_offset = 0 ! For scrolling through long lists |
| 22 | integer :: diagnostic_count = 0 |
| 23 | type(diagnostic_t), allocatable :: diagnostics(:) |
| 24 | end type diagnostics_panel_t |
| 25 | |
| 26 | contains |
| 27 | |
| 28 | subroutine init_diagnostics_panel(panel) |
| 29 | type(diagnostics_panel_t), intent(out) :: panel |
| 30 | panel%visible = .false. |
| 31 | panel%width = 40 |
| 32 | panel%selected_index = 1 |
| 33 | panel%scroll_offset = 0 |
| 34 | panel%diagnostic_count = 0 |
| 35 | if (allocated(panel%diagnostics)) deallocate(panel%diagnostics) |
| 36 | end subroutine init_diagnostics_panel |
| 37 | |
| 38 | subroutine cleanup_diagnostics_panel(panel) |
| 39 | type(diagnostics_panel_t), intent(inout) :: panel |
| 40 | if (allocated(panel%diagnostics)) deallocate(panel%diagnostics) |
| 41 | panel%diagnostic_count = 0 |
| 42 | end subroutine cleanup_diagnostics_panel |
| 43 | |
| 44 | subroutine toggle_diagnostics_panel(panel) |
| 45 | type(diagnostics_panel_t), intent(inout) :: panel |
| 46 | |
| 47 | panel%visible = .not. panel%visible |
| 48 | |
| 49 | if (panel%visible) then |
| 50 | panel%selected_index = 1 |
| 51 | panel%scroll_offset = 0 |
| 52 | end if |
| 53 | end subroutine toggle_diagnostics_panel |
| 54 | |
| 55 | function is_diagnostics_panel_visible(panel) result(visible) |
| 56 | type(diagnostics_panel_t), intent(in) :: panel |
| 57 | logical :: visible |
| 58 | visible = panel%visible |
| 59 | end function is_diagnostics_panel_visible |
| 60 | |
| 61 | subroutine update_diagnostics(panel, diagnostics_store, file_uri) |
| 62 | type(diagnostics_panel_t), intent(inout) :: panel |
| 63 | type(diagnostics_store_t), intent(in) :: diagnostics_store |
| 64 | character(len=*), intent(in) :: file_uri |
| 65 | |
| 66 | ! Get all diagnostics for current file |
| 67 | if (allocated(panel%diagnostics)) deallocate(panel%diagnostics) |
| 68 | panel%diagnostics = get_diagnostics_for_file(diagnostics_store, file_uri) |
| 69 | |
| 70 | if (allocated(panel%diagnostics)) then |
| 71 | panel%diagnostic_count = size(panel%diagnostics) |
| 72 | else |
| 73 | panel%diagnostic_count = 0 |
| 74 | end if |
| 75 | |
| 76 | ! Reset selection if out of bounds |
| 77 | if (panel%selected_index > panel%diagnostic_count) then |
| 78 | panel%selected_index = max(1, panel%diagnostic_count) |
| 79 | end if |
| 80 | end subroutine update_diagnostics |
| 81 | |
| 82 | subroutine render_diagnostics_panel(panel, diagnostics_store, file_uri, screen_rows, screen_cols) |
| 83 | type(diagnostics_panel_t), intent(inout) :: panel |
| 84 | type(diagnostics_store_t), intent(in) :: diagnostics_store |
| 85 | character(len=*), intent(in) :: file_uri |
| 86 | integer, intent(in) :: screen_rows, screen_cols |
| 87 | integer :: start_col, row, i, visible_items |
| 88 | character(len=256) :: line_buffer |
| 89 | character(len=5) :: severity_marker |
| 90 | character(len=10) :: severity_color |
| 91 | |
| 92 | ! Initialize line_buffer to prevent garbage characters |
| 93 | line_buffer = repeat(' ', len(line_buffer)) |
| 94 | |
| 95 | if (.not. panel%visible) return |
| 96 | |
| 97 | ! Update diagnostics |
| 98 | call update_diagnostics(panel, diagnostics_store, file_uri) |
| 99 | |
| 100 | ! Calculate panel position (right side) |
| 101 | start_col = screen_cols - panel%width + 1 |
| 102 | if (start_col < 1) start_col = 1 |
| 103 | |
| 104 | ! Draw panel border and title |
| 105 | row = 1 |
| 106 | call terminal_move_cursor(row, start_col) |
| 107 | |
| 108 | ! Top border with title |
| 109 | call terminal_write(char(27) // '[48;5;236m') ! Dark background |
| 110 | write(line_buffer, '(A,I0,A)') ' Diagnostics (', panel%diagnostic_count, ') ' |
| 111 | call terminal_write(char(27) // '[1m' // trim(line_buffer) // char(27) // '[0m') |
| 112 | |
| 113 | ! Separator |
| 114 | row = row + 1 |
| 115 | call terminal_move_cursor(row, start_col) |
| 116 | call terminal_write(char(27) // '[48;5;236m' // repeat("─", panel%width) // char(27) // '[0m') |
| 117 | |
| 118 | ! Content area |
| 119 | visible_items = min(panel%diagnostic_count, screen_rows - 3) |
| 120 | row = row + 1 |
| 121 | |
| 122 | ! Display diagnostics or "No diagnostics" message |
| 123 | if (panel%diagnostic_count == 0) then |
| 124 | call terminal_move_cursor(row, start_col) |
| 125 | call terminal_write(char(27) // '[48;5;235m' // char(27) // '[90m') |
| 126 | call terminal_write(' No diagnostics found') |
| 127 | call terminal_write(char(27) // '[K') ! Clear to end of line |
| 128 | call terminal_write(char(27) // '[0m') |
| 129 | return |
| 130 | end if |
| 131 | |
| 132 | ! Render diagnostics with wrapping for selected item |
| 133 | block |
| 134 | integer :: screen_line, diag_idx, wrap_line |
| 135 | integer :: max_content_lines |
| 136 | logical :: is_selected |
| 137 | |
| 138 | screen_line = 0 |
| 139 | diag_idx = 1 |
| 140 | max_content_lines = screen_rows - 4 ! Leave room for header and footer |
| 141 | |
| 142 | do while (screen_line < max_content_lines .and. diag_idx <= panel%diagnostic_count) |
| 143 | is_selected = (diag_idx == panel%selected_index) |
| 144 | |
| 145 | ! Get severity marker and color |
| 146 | call get_severity_display(panel%diagnostics(diag_idx)%severity, & |
| 147 | severity_marker, severity_color) |
| 148 | |
| 149 | ! Clear line_buffer to prevent leftover characters |
| 150 | line_buffer = repeat(' ', len(line_buffer)) |
| 151 | |
| 152 | ! Format diagnostic header line (severity + line number) |
| 153 | write(line_buffer, '(A2,A5,A,I0,A,I0,A)') & |
| 154 | ' ', severity_marker, ' L', & |
| 155 | panel%diagnostics(diag_idx)%range%start_line + 1, ':', & |
| 156 | panel%diagnostics(diag_idx)%range%start_col + 1, ' ' |
| 157 | |
| 158 | if (is_selected) then |
| 159 | ! Selected: show full message wrapped |
| 160 | block |
| 161 | integer :: header_len, first_line_chars, msg_len, remaining_len |
| 162 | integer :: chars_per_line, total_lines |
| 163 | character(len=512) :: full_message |
| 164 | |
| 165 | full_message = panel%diagnostics(diag_idx)%message |
| 166 | msg_len = len_trim(full_message) |
| 167 | header_len = len_trim(line_buffer) |
| 168 | first_line_chars = panel%width - header_len - 1 ! Space available on first line |
| 169 | chars_per_line = panel%width - 4 ! Continuation lines have 4-char indent |
| 170 | |
| 171 | ! Render first line with header + start of message |
| 172 | screen_line = screen_line + 1 |
| 173 | |
| 174 | call terminal_move_cursor(row + screen_line - 1, start_col) |
| 175 | call terminal_write(char(27) // '[48;5;240m') ! Highlight |
| 176 | call terminal_write(trim(severity_color)) |
| 177 | |
| 178 | ! Append first portion of message (no truncation marker) |
| 179 | if (first_line_chars > 0 .and. msg_len > 0) then |
| 180 | if (msg_len <= first_line_chars) then |
| 181 | line_buffer(header_len+1:header_len+msg_len) = full_message(1:msg_len) |
| 182 | else |
| 183 | line_buffer(header_len+1:header_len+first_line_chars) = & |
| 184 | full_message(1:first_line_chars) |
| 185 | end if |
| 186 | end if |
| 187 | |
| 188 | ! Pad to width |
| 189 | do i = len_trim(line_buffer) + 1, panel%width |
| 190 | line_buffer(i:i) = ' ' |
| 191 | end do |
| 192 | |
| 193 | call terminal_write(line_buffer(1:panel%width)) |
| 194 | call terminal_write(char(27) // '[0m') |
| 195 | |
| 196 | ! Calculate and render continuation lines for remaining message |
| 197 | remaining_len = msg_len - first_line_chars |
| 198 | if (remaining_len > 0) then |
| 199 | total_lines = (remaining_len + chars_per_line - 1) / chars_per_line |
| 200 | do wrap_line = 1, total_lines |
| 201 | if (screen_line >= max_content_lines) exit |
| 202 | screen_line = screen_line + 1 |
| 203 | |
| 204 | call terminal_move_cursor(row + screen_line - 1, start_col) |
| 205 | |
| 206 | ! Render continuation line |
| 207 | block |
| 208 | character(len=256) :: cont_buffer |
| 209 | integer :: cont_start, cont_end, k |
| 210 | |
| 211 | cont_buffer = repeat(' ', len(cont_buffer)) |
| 212 | cont_buffer(1:4) = ' ' ! Indent |
| 213 | |
| 214 | cont_start = first_line_chars + (wrap_line - 1) * chars_per_line + 1 |
| 215 | cont_end = min(cont_start + chars_per_line - 1, msg_len) |
| 216 | |
| 217 | if (cont_start <= msg_len) then |
| 218 | cont_buffer(5:5 + cont_end - cont_start) = & |
| 219 | full_message(cont_start:cont_end) |
| 220 | end if |
| 221 | |
| 222 | ! Pad |
| 223 | do k = len_trim(cont_buffer) + 1, panel%width |
| 224 | cont_buffer(k:k) = ' ' |
| 225 | end do |
| 226 | |
| 227 | call terminal_write(char(27) // '[48;5;240m') ! Highlight |
| 228 | call terminal_write(cont_buffer(1:panel%width)) |
| 229 | call terminal_write(char(27) // '[0m') |
| 230 | end block |
| 231 | end do |
| 232 | end if |
| 233 | end block |
| 234 | else |
| 235 | ! Not selected: single truncated line |
| 236 | screen_line = screen_line + 1 |
| 237 | call terminal_move_cursor(row + screen_line - 1, start_col) |
| 238 | call terminal_write(char(27) // '[48;5;235m') ! Normal |
| 239 | call terminal_write(trim(severity_color)) |
| 240 | call append_truncated_message(line_buffer, & |
| 241 | panel%diagnostics(diag_idx)%message, panel%width) |
| 242 | call terminal_write(line_buffer(1:panel%width)) |
| 243 | call terminal_write(char(27) // '[0m') |
| 244 | end if |
| 245 | |
| 246 | diag_idx = diag_idx + 1 |
| 247 | end do |
| 248 | |
| 249 | ! Fill remaining lines with empty space |
| 250 | do while (screen_line < max_content_lines) |
| 251 | screen_line = screen_line + 1 |
| 252 | call terminal_move_cursor(row + screen_line - 1, start_col) |
| 253 | call render_empty_line(panel%width) |
| 254 | end do |
| 255 | end block |
| 256 | |
| 257 | ! Show count in bottom right corner |
| 258 | if (panel%diagnostic_count > 0) then |
| 259 | call terminal_move_cursor(screen_rows - 1, start_col + 2) |
| 260 | write(line_buffer, '(A,I0,A,I0,A)') '[', panel%selected_index, '/', & |
| 261 | panel%diagnostic_count, ']' |
| 262 | call terminal_write(char(27) // '[48;5;236m') |
| 263 | call terminal_write(trim(line_buffer)) |
| 264 | call terminal_write(char(27) // '[0m') |
| 265 | end if |
| 266 | |
| 267 | end subroutine render_diagnostics_panel |
| 268 | |
| 269 | subroutine render_empty_line(width) |
| 270 | integer, intent(in) :: width |
| 271 | |
| 272 | call terminal_write(char(27) // '[48;5;235m') ! Dark background |
| 273 | call terminal_write(repeat(' ', width)) |
| 274 | call terminal_write(char(27) // '[0m') |
| 275 | end subroutine render_empty_line |
| 276 | |
| 277 | subroutine get_severity_display(severity, marker, color) |
| 278 | integer, intent(in) :: severity |
| 279 | character(len=5), intent(out) :: marker |
| 280 | character(len=10), intent(out) :: color |
| 281 | |
| 282 | select case(severity) |
| 283 | case(SEVERITY_ERROR) |
| 284 | marker = '●' |
| 285 | color = char(27) // '[31m' ! Red |
| 286 | case(SEVERITY_WARNING) |
| 287 | marker = '▲' |
| 288 | color = char(27) // '[33m' ! Yellow |
| 289 | case(SEVERITY_INFO) |
| 290 | marker = '◆' |
| 291 | color = char(27) // '[36m' ! Cyan |
| 292 | case(SEVERITY_HINT) |
| 293 | marker = '○' |
| 294 | color = char(27) // '[90m' ! Gray |
| 295 | case default |
| 296 | marker = ' ' |
| 297 | color = '' |
| 298 | end select |
| 299 | end subroutine get_severity_display |
| 300 | |
| 301 | subroutine append_truncated_message(buffer, message, max_len) |
| 302 | character(len=*), intent(inout) :: buffer |
| 303 | character(len=*), intent(in) :: message |
| 304 | integer, intent(in) :: max_len |
| 305 | integer :: current_len, msg_start, available_space |
| 306 | |
| 307 | current_len = len_trim(buffer) |
| 308 | msg_start = current_len + 1 |
| 309 | available_space = max_len - current_len - 1 ! -1 for border |
| 310 | |
| 311 | if (available_space > 3) then |
| 312 | if (len_trim(message) <= available_space) then |
| 313 | buffer(msg_start:) = message |
| 314 | else |
| 315 | buffer(msg_start:msg_start + available_space - 4) = & |
| 316 | message(1:available_space - 3) |
| 317 | buffer(msg_start + available_space - 3:) = '...' |
| 318 | end if |
| 319 | end if |
| 320 | |
| 321 | ! Pad to width |
| 322 | current_len = len_trim(buffer) |
| 323 | do while (current_len < max_len - 1) |
| 324 | current_len = current_len + 1 |
| 325 | buffer(current_len:current_len) = ' ' |
| 326 | end do |
| 327 | end subroutine append_truncated_message |
| 328 | |
| 329 | ! UNUSED: Calculate how many lines a message needs when wrapped |
| 330 | ! Kept for potential future use |
| 331 | ! function calc_wrapped_lines(message, width) result(num_lines) |
| 332 | ! character(len=*), intent(in) :: message |
| 333 | ! integer, intent(in) :: width |
| 334 | ! integer :: num_lines |
| 335 | ! integer :: msg_len, wrap_width |
| 336 | ! |
| 337 | ! msg_len = len_trim(message) |
| 338 | ! wrap_width = width - 4 ! Leave space for indentation |
| 339 | ! |
| 340 | ! if (msg_len <= wrap_width) then |
| 341 | ! num_lines = 1 |
| 342 | ! else |
| 343 | ! num_lines = (msg_len + wrap_width - 1) / wrap_width |
| 344 | ! end if |
| 345 | ! end function calc_wrapped_lines |
| 346 | |
| 347 | ! UNUSED: Render a wrapped line of a message (line_num is 1-based) |
| 348 | ! Kept for potential future use |
| 349 | ! subroutine render_wrapped_line(message, line_num, width, start_col, is_selected) |
| 350 | ! character(len=*), intent(in) :: message |
| 351 | ! integer, intent(in) :: line_num, width, start_col |
| 352 | ! logical, intent(in) :: is_selected |
| 353 | ! character(len=256) :: output_buffer |
| 354 | ! integer :: msg_len, wrap_width, start_pos, end_pos, i |
| 355 | ! |
| 356 | ! msg_len = len_trim(message) |
| 357 | ! wrap_width = width - 4 ! Leave space for indentation |
| 358 | ! |
| 359 | ! ! Calculate which portion of message to show |
| 360 | ! start_pos = (line_num - 1) * wrap_width + 1 |
| 361 | ! end_pos = min(start_pos + wrap_width - 1, msg_len) |
| 362 | ! |
| 363 | ! ! Initialize buffer with spaces |
| 364 | ! output_buffer = repeat(' ', len(output_buffer)) |
| 365 | ! |
| 366 | ! ! Add indentation for continuation lines |
| 367 | ! output_buffer(1:4) = ' ' |
| 368 | ! |
| 369 | ! ! Copy message portion |
| 370 | ! if (start_pos <= msg_len) then |
| 371 | ! output_buffer(5:5 + end_pos - start_pos) = message(start_pos:end_pos) |
| 372 | ! end if |
| 373 | ! |
| 374 | ! ! Pad to width |
| 375 | ! do i = len_trim(output_buffer) + 1, width |
| 376 | ! output_buffer(i:i) = ' ' |
| 377 | ! end do |
| 378 | ! |
| 379 | ! ! Set background color |
| 380 | ! if (is_selected) then |
| 381 | ! call terminal_write(char(27) // '[48;5;240m') ! Highlight |
| 382 | ! else |
| 383 | ! call terminal_write(char(27) // '[48;5;235m') ! Normal |
| 384 | ! end if |
| 385 | ! |
| 386 | ! ! Write the line |
| 387 | ! call terminal_write(output_buffer(1:width)) |
| 388 | ! call terminal_write(char(27) // '[0m') |
| 389 | ! end subroutine render_wrapped_line |
| 390 | |
| 391 | function diagnostics_panel_handle_key(panel, key) result(handled) |
| 392 | type(diagnostics_panel_t), intent(inout) :: panel |
| 393 | character(len=*), intent(in) :: key |
| 394 | logical :: handled |
| 395 | |
| 396 | handled = .false. |
| 397 | if (.not. panel%visible) return |
| 398 | |
| 399 | select case(key) |
| 400 | case('j', 'down') ! j or down arrow |
| 401 | if (panel%selected_index < panel%diagnostic_count) then |
| 402 | panel%selected_index = panel%selected_index + 1 |
| 403 | end if |
| 404 | handled = .true. ! Always consume navigation keys |
| 405 | |
| 406 | case('k', 'up') ! k or up arrow |
| 407 | if (panel%selected_index > 1) then |
| 408 | panel%selected_index = panel%selected_index - 1 |
| 409 | end if |
| 410 | handled = .true. ! Always consume navigation keys |
| 411 | |
| 412 | case('enter') ! Enter - jump to diagnostic |
| 413 | ! This will need to be handled by the main editor |
| 414 | handled = .true. |
| 415 | |
| 416 | case('esc', 'q') ! ESC or q - close panel |
| 417 | panel%visible = .false. |
| 418 | handled = .true. |
| 419 | |
| 420 | end select |
| 421 | end function diagnostics_panel_handle_key |
| 422 | |
| 423 | ! UNUSED: Get location of selected diagnostic |
| 424 | ! Kept for potential future use |
| 425 | ! function get_selected_diagnostic_location(panel, line, col) result(has_location) |
| 426 | ! type(diagnostics_panel_t), intent(in) :: panel |
| 427 | ! integer, intent(out) :: line, col |
| 428 | ! logical :: has_location |
| 429 | ! |
| 430 | ! has_location = .false. |
| 431 | ! line = 1 |
| 432 | ! col = 1 |
| 433 | ! |
| 434 | ! if (panel%visible .and. panel%diagnostic_count > 0 .and. & |
| 435 | ! panel%selected_index > 0 .and. panel%selected_index <= panel%diagnostic_count) then |
| 436 | ! |
| 437 | ! line = panel%diagnostics(panel%selected_index)%range%start_line + 1 ! Convert to 1-based |
| 438 | ! col = panel%diagnostics(panel%selected_index)%range%start_col + 1 |
| 439 | ! has_location = .true. |
| 440 | ! end if |
| 441 | ! end function get_selected_diagnostic_location |
| 442 | |
| 443 | end module diagnostics_panel_module |