| 1 | module code_actions_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 :: code_actions_panel_t, code_action_t |
| 8 | public :: init_code_actions_panel, cleanup_code_actions_panel |
| 9 | public :: show_code_actions_panel, hide_code_actions_panel |
| 10 | public :: toggle_code_actions_panel |
| 11 | public :: is_code_actions_panel_visible, code_actions_panel_handle_key |
| 12 | public :: set_code_actions, clear_code_actions |
| 13 | public :: get_selected_action, render_code_actions_panel |
| 14 | |
| 15 | ! Code action type |
| 16 | type :: code_action_t |
| 17 | character(len=:), allocatable :: title |
| 18 | character(len=:), allocatable :: kind ! quickfix, refactor, etc. |
| 19 | character(len=:), allocatable :: command |
| 20 | logical :: is_preferred = .false. |
| 21 | ! Store the full action JSON for applying later |
| 22 | character(len=:), allocatable :: action_json |
| 23 | end type code_action_t |
| 24 | |
| 25 | ! Code actions panel (offcanvas on right side) |
| 26 | type :: code_actions_panel_t |
| 27 | logical :: visible = .false. |
| 28 | integer :: width = 45 ! Panel width in columns |
| 29 | integer :: selected_index = 1 |
| 30 | integer :: scroll_offset = 0 |
| 31 | |
| 32 | ! Actions data |
| 33 | type(code_action_t), allocatable :: actions(:) |
| 34 | integer :: num_actions = 0 |
| 35 | end type code_actions_panel_t |
| 36 | |
| 37 | contains |
| 38 | |
| 39 | subroutine init_code_actions_panel(panel) |
| 40 | type(code_actions_panel_t), intent(out) :: panel |
| 41 | |
| 42 | panel%visible = .false. |
| 43 | panel%width = 45 |
| 44 | panel%selected_index = 1 |
| 45 | panel%scroll_offset = 0 |
| 46 | panel%num_actions = 0 |
| 47 | end subroutine init_code_actions_panel |
| 48 | |
| 49 | subroutine cleanup_code_actions_panel(panel) |
| 50 | type(code_actions_panel_t), intent(inout) :: panel |
| 51 | integer :: i |
| 52 | |
| 53 | if (allocated(panel%actions)) then |
| 54 | do i = 1, panel%num_actions |
| 55 | if (allocated(panel%actions(i)%title)) deallocate(panel%actions(i)%title) |
| 56 | if (allocated(panel%actions(i)%kind)) deallocate(panel%actions(i)%kind) |
| 57 | if (allocated(panel%actions(i)%command)) deallocate(panel%actions(i)%command) |
| 58 | if (allocated(panel%actions(i)%action_json)) deallocate(panel%actions(i)%action_json) |
| 59 | end do |
| 60 | deallocate(panel%actions) |
| 61 | end if |
| 62 | |
| 63 | panel%num_actions = 0 |
| 64 | panel%selected_index = 1 |
| 65 | panel%scroll_offset = 0 |
| 66 | end subroutine cleanup_code_actions_panel |
| 67 | |
| 68 | subroutine set_code_actions(panel, actions, num_actions) |
| 69 | type(code_actions_panel_t), intent(inout) :: panel |
| 70 | type(code_action_t), intent(in) :: actions(:) |
| 71 | integer, intent(in) :: num_actions |
| 72 | integer :: i |
| 73 | |
| 74 | ! Clear existing actions |
| 75 | call cleanup_code_actions_panel(panel) |
| 76 | |
| 77 | if (num_actions > 0) then |
| 78 | allocate(panel%actions(num_actions)) |
| 79 | panel%num_actions = num_actions |
| 80 | |
| 81 | do i = 1, num_actions |
| 82 | if (allocated(actions(i)%title)) then |
| 83 | allocate(character(len=len(actions(i)%title)) :: panel%actions(i)%title) |
| 84 | panel%actions(i)%title = actions(i)%title |
| 85 | end if |
| 86 | |
| 87 | if (allocated(actions(i)%kind)) then |
| 88 | allocate(character(len=len(actions(i)%kind)) :: panel%actions(i)%kind) |
| 89 | panel%actions(i)%kind = actions(i)%kind |
| 90 | end if |
| 91 | |
| 92 | if (allocated(actions(i)%command)) then |
| 93 | allocate(character(len=len(actions(i)%command)) :: panel%actions(i)%command) |
| 94 | panel%actions(i)%command = actions(i)%command |
| 95 | end if |
| 96 | |
| 97 | if (allocated(actions(i)%action_json)) then |
| 98 | allocate(character(len=len(actions(i)%action_json)) :: panel%actions(i)%action_json) |
| 99 | panel%actions(i)%action_json = actions(i)%action_json |
| 100 | end if |
| 101 | |
| 102 | panel%actions(i)%is_preferred = actions(i)%is_preferred |
| 103 | end do |
| 104 | end if |
| 105 | |
| 106 | panel%selected_index = 1 |
| 107 | panel%scroll_offset = 0 |
| 108 | end subroutine set_code_actions |
| 109 | |
| 110 | subroutine clear_code_actions(panel) |
| 111 | type(code_actions_panel_t), intent(inout) :: panel |
| 112 | call cleanup_code_actions_panel(panel) |
| 113 | end subroutine clear_code_actions |
| 114 | |
| 115 | subroutine show_code_actions_panel(panel) |
| 116 | type(code_actions_panel_t), intent(inout) :: panel |
| 117 | panel%visible = .true. |
| 118 | panel%selected_index = 1 |
| 119 | panel%scroll_offset = 0 |
| 120 | end subroutine show_code_actions_panel |
| 121 | |
| 122 | subroutine hide_code_actions_panel(panel) |
| 123 | type(code_actions_panel_t), intent(inout) :: panel |
| 124 | panel%visible = .false. |
| 125 | end subroutine hide_code_actions_panel |
| 126 | |
| 127 | subroutine toggle_code_actions_panel(panel) |
| 128 | type(code_actions_panel_t), intent(inout) :: panel |
| 129 | panel%visible = .not. panel%visible |
| 130 | if (panel%visible) then |
| 131 | panel%selected_index = 1 |
| 132 | panel%scroll_offset = 0 |
| 133 | end if |
| 134 | end subroutine toggle_code_actions_panel |
| 135 | |
| 136 | function is_code_actions_panel_visible(panel) result(visible) |
| 137 | type(code_actions_panel_t), intent(in) :: panel |
| 138 | logical :: visible |
| 139 | visible = panel%visible |
| 140 | end function is_code_actions_panel_visible |
| 141 | |
| 142 | subroutine render_code_actions_panel(panel, screen_rows, screen_cols) |
| 143 | type(code_actions_panel_t), intent(in) :: panel |
| 144 | integer, intent(in) :: screen_rows, screen_cols |
| 145 | integer :: start_col, row, i, max_content_lines |
| 146 | character(len=256) :: line_buffer |
| 147 | character(len=10) :: kind_icon |
| 148 | character(len=10) :: kind_color |
| 149 | |
| 150 | if (.not. panel%visible) return |
| 151 | |
| 152 | ! Calculate panel position (right side) |
| 153 | start_col = screen_cols - panel%width + 1 |
| 154 | if (start_col < 1) start_col = 1 |
| 155 | |
| 156 | ! Initialize line buffer |
| 157 | line_buffer = repeat(' ', len(line_buffer)) |
| 158 | |
| 159 | ! Draw panel header |
| 160 | row = 1 |
| 161 | call terminal_move_cursor(row, start_col) |
| 162 | call terminal_write(char(27) // '[48;5;236m') ! Dark background |
| 163 | write(line_buffer, '(A,I0,A)') ' Code Actions (', panel%num_actions, ') ' |
| 164 | call terminal_write(char(27) // '[1m' // trim(line_buffer) // char(27) // '[0m') |
| 165 | |
| 166 | ! Pad header to width |
| 167 | call terminal_move_cursor(row, start_col + len_trim(line_buffer)) |
| 168 | call terminal_write(char(27) // '[48;5;236m' // repeat(' ', panel%width - len_trim(line_buffer)) // char(27) // '[0m') |
| 169 | |
| 170 | ! Separator |
| 171 | row = row + 1 |
| 172 | call terminal_move_cursor(row, start_col) |
| 173 | call terminal_write(char(27) // '[48;5;236m' // repeat('-', panel%width) // char(27) // '[0m') |
| 174 | |
| 175 | ! Content area |
| 176 | row = row + 1 |
| 177 | max_content_lines = screen_rows - 4 |
| 178 | |
| 179 | ! Display "No code actions" if empty |
| 180 | if (panel%num_actions == 0) then |
| 181 | call terminal_move_cursor(row, start_col) |
| 182 | call terminal_write(char(27) // '[48;5;235m' // char(27) // '[90m') |
| 183 | line_buffer = ' No code actions available' |
| 184 | call pad_to_width(line_buffer, panel%width) |
| 185 | call terminal_write(line_buffer(1:panel%width)) |
| 186 | call terminal_write(char(27) // '[0m') |
| 187 | |
| 188 | ! Fill remaining lines |
| 189 | do i = row + 1, screen_rows - 1 |
| 190 | call terminal_move_cursor(i, start_col) |
| 191 | call terminal_write(char(27) // '[48;5;235m' // repeat(' ', panel%width) // char(27) // '[0m') |
| 192 | end do |
| 193 | return |
| 194 | end if |
| 195 | |
| 196 | ! Render code actions |
| 197 | do i = 1, min(panel%num_actions, max_content_lines) |
| 198 | call terminal_move_cursor(row, start_col) |
| 199 | line_buffer = repeat(' ', len(line_buffer)) |
| 200 | |
| 201 | ! Get icon and color based on kind |
| 202 | call get_action_display(panel%actions(i)%kind, kind_icon, kind_color) |
| 203 | |
| 204 | ! Highlight selected item |
| 205 | if (i == panel%selected_index) then |
| 206 | call terminal_write(char(27) // '[48;5;240m') ! Highlight background |
| 207 | else |
| 208 | call terminal_write(char(27) // '[48;5;235m') ! Normal background |
| 209 | end if |
| 210 | |
| 211 | ! Format: " [icon] Title " |
| 212 | if (i <= 9) then |
| 213 | write(line_buffer, '(A1,I1,A,A,A,A)') ' ', i, '. ', trim(kind_icon), ' ', trim(panel%actions(i)%title) |
| 214 | else |
| 215 | write(line_buffer, '(A,A,A,A)') ' ', trim(kind_icon), ' ', trim(panel%actions(i)%title) |
| 216 | end if |
| 217 | |
| 218 | ! Add preferred indicator |
| 219 | if (panel%actions(i)%is_preferred) then |
| 220 | line_buffer = trim(line_buffer) // ' *' |
| 221 | end if |
| 222 | |
| 223 | ! Truncate if too long |
| 224 | if (len_trim(line_buffer) > panel%width - 1) then |
| 225 | line_buffer = line_buffer(1:panel%width - 4) // '...' |
| 226 | end if |
| 227 | |
| 228 | ! Pad to width |
| 229 | call pad_to_width(line_buffer, panel%width) |
| 230 | |
| 231 | ! Apply kind color to icon |
| 232 | call terminal_write(trim(kind_color)) |
| 233 | call terminal_write(line_buffer(1:panel%width)) |
| 234 | call terminal_write(char(27) // '[0m') |
| 235 | |
| 236 | row = row + 1 |
| 237 | end do |
| 238 | |
| 239 | ! Fill remaining lines with empty background |
| 240 | do i = row, screen_rows - 1 |
| 241 | call terminal_move_cursor(i, start_col) |
| 242 | call terminal_write(char(27) // '[48;5;235m' // repeat(' ', panel%width) // char(27) // '[0m') |
| 243 | end do |
| 244 | |
| 245 | ! Footer with hints |
| 246 | call terminal_move_cursor(screen_rows - 1, start_col) |
| 247 | call terminal_write(char(27) // '[48;5;236m' // char(27) // '[90m') |
| 248 | line_buffer = ' j/k:Navigate Enter:Apply Esc:Close' |
| 249 | call pad_to_width(line_buffer, panel%width) |
| 250 | call terminal_write(line_buffer(1:panel%width)) |
| 251 | call terminal_write(char(27) // '[0m') |
| 252 | |
| 253 | end subroutine render_code_actions_panel |
| 254 | |
| 255 | subroutine get_action_display(kind, icon, color) |
| 256 | character(len=*), intent(in), optional :: kind |
| 257 | character(len=10), intent(out) :: icon |
| 258 | character(len=10), intent(out) :: color |
| 259 | |
| 260 | icon = '' |
| 261 | color = '' |
| 262 | |
| 263 | if (.not. present(kind)) return |
| 264 | if (len_trim(kind) == 0) return |
| 265 | |
| 266 | select case(trim(kind)) |
| 267 | case('quickfix') |
| 268 | icon = '[fix]' |
| 269 | color = char(27) // '[33m' ! Yellow |
| 270 | case('refactor') |
| 271 | icon = '[ref]' |
| 272 | color = char(27) // '[36m' ! Cyan |
| 273 | case('refactor.extract') |
| 274 | icon = '[ext]' |
| 275 | color = char(27) // '[36m' |
| 276 | case('refactor.inline') |
| 277 | icon = '[inl]' |
| 278 | color = char(27) // '[36m' |
| 279 | case('refactor.rewrite') |
| 280 | icon = '[rw]' |
| 281 | color = char(27) // '[36m' |
| 282 | case('source') |
| 283 | icon = '[src]' |
| 284 | color = char(27) // '[32m' ! Green |
| 285 | case('source.organizeImports', 'source.organizeImports.ruff') |
| 286 | icon = '[org]' |
| 287 | color = char(27) // '[32m' |
| 288 | case('source.fixAll', 'source.fixAll.ruff') |
| 289 | icon = '[all]' |
| 290 | color = char(27) // '[32m' |
| 291 | case default |
| 292 | icon = '' |
| 293 | color = char(27) // '[37m' ! White |
| 294 | end select |
| 295 | end subroutine get_action_display |
| 296 | |
| 297 | subroutine pad_to_width(buffer, width) |
| 298 | character(len=*), intent(inout) :: buffer |
| 299 | integer, intent(in) :: width |
| 300 | integer :: current_len, i |
| 301 | |
| 302 | current_len = len_trim(buffer) |
| 303 | do i = current_len + 1, width |
| 304 | if (i <= len(buffer)) buffer(i:i) = ' ' |
| 305 | end do |
| 306 | end subroutine pad_to_width |
| 307 | |
| 308 | function code_actions_panel_handle_key(panel, key) result(handled) |
| 309 | type(code_actions_panel_t), intent(inout) :: panel |
| 310 | character(len=*), intent(in) :: key |
| 311 | logical :: handled |
| 312 | integer :: num |
| 313 | |
| 314 | handled = .false. |
| 315 | if (.not. panel%visible) return |
| 316 | |
| 317 | select case(trim(key)) |
| 318 | case('j', 'down') |
| 319 | if (panel%selected_index < panel%num_actions) then |
| 320 | panel%selected_index = panel%selected_index + 1 |
| 321 | end if |
| 322 | handled = .true. |
| 323 | |
| 324 | case('k', 'up') |
| 325 | if (panel%selected_index > 1) then |
| 326 | panel%selected_index = panel%selected_index - 1 |
| 327 | end if |
| 328 | handled = .true. |
| 329 | |
| 330 | case('1':'9') |
| 331 | ! Quick select by number |
| 332 | read(key, '(I1)') num |
| 333 | if (num <= panel%num_actions) then |
| 334 | panel%selected_index = num |
| 335 | end if |
| 336 | handled = .true. |
| 337 | |
| 338 | case('enter') |
| 339 | ! Action will be applied by caller |
| 340 | handled = .true. |
| 341 | |
| 342 | case('escape', 'esc', 'q') |
| 343 | panel%visible = .false. |
| 344 | handled = .true. |
| 345 | end select |
| 346 | end function code_actions_panel_handle_key |
| 347 | |
| 348 | function get_selected_action(panel, action_json) result(has_action) |
| 349 | type(code_actions_panel_t), intent(in) :: panel |
| 350 | character(len=:), allocatable, intent(out) :: action_json |
| 351 | logical :: has_action |
| 352 | |
| 353 | has_action = .false. |
| 354 | |
| 355 | if (panel%selected_index > 0 .and. panel%selected_index <= panel%num_actions) then |
| 356 | if (allocated(panel%actions(panel%selected_index)%action_json)) then |
| 357 | allocate(character(len=len(panel%actions(panel%selected_index)%action_json)) :: action_json) |
| 358 | action_json = panel%actions(panel%selected_index)%action_json |
| 359 | has_action = .true. |
| 360 | end if |
| 361 | end if |
| 362 | end function get_selected_action |
| 363 | |
| 364 | end module code_actions_panel_module |
| 365 |