Fortran · 18414 bytes Raw Blame History
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