| 1 | module completion_popup_module |
| 2 | use iso_fortran_env, only: int32 |
| 3 | use terminal_io_module, only: terminal_move_cursor, terminal_write |
| 4 | use json_module, only: json_value_t, json_get_string, & |
| 5 | json_get_object, json_has_key, & |
| 6 | json_array_size, json_get_array_element, & |
| 7 | json_get_array, json_get_number |
| 8 | implicit none |
| 9 | private |
| 10 | |
| 11 | public :: completion_popup_t |
| 12 | public :: init_completion_popup, cleanup_completion_popup |
| 13 | public :: show_completion_popup, hide_completion_popup |
| 14 | public :: handle_completion_response |
| 15 | public :: navigate_completion_up, navigate_completion_down |
| 16 | public :: get_selected_completion, is_completion_visible |
| 17 | |
| 18 | integer, parameter :: MAX_VISIBLE_ITEMS = 10 |
| 19 | integer, parameter :: MAX_LABEL_WIDTH = 40 |
| 20 | |
| 21 | type :: completion_item_t |
| 22 | character(len=:), allocatable :: label ! Main text to display |
| 23 | character(len=:), allocatable :: kind ! Variable, Function, etc. |
| 24 | character(len=:), allocatable :: detail ! Additional info |
| 25 | character(len=:), allocatable :: insert_text ! Text to insert when selected |
| 26 | end type completion_item_t |
| 27 | |
| 28 | type :: completion_popup_t |
| 29 | type(completion_item_t), allocatable :: items(:) |
| 30 | integer :: item_count = 0 |
| 31 | integer :: selected_index = 1 |
| 32 | integer :: scroll_offset = 0 |
| 33 | logical :: visible = .false. |
| 34 | |
| 35 | ! Position on screen |
| 36 | integer :: row = 0 |
| 37 | integer :: col = 0 |
| 38 | integer :: width = 0 |
| 39 | integer :: height = 0 |
| 40 | end type completion_popup_t |
| 41 | |
| 42 | contains |
| 43 | |
| 44 | subroutine init_completion_popup(popup) |
| 45 | type(completion_popup_t), intent(out) :: popup |
| 46 | |
| 47 | popup%item_count = 0 |
| 48 | popup%selected_index = 1 |
| 49 | popup%scroll_offset = 0 |
| 50 | popup%visible = .false. |
| 51 | popup%row = 0 |
| 52 | popup%col = 0 |
| 53 | popup%width = MAX_LABEL_WIDTH + 10 |
| 54 | popup%height = 0 |
| 55 | |
| 56 | if (allocated(popup%items)) deallocate(popup%items) |
| 57 | end subroutine init_completion_popup |
| 58 | |
| 59 | subroutine cleanup_completion_popup(popup) |
| 60 | type(completion_popup_t), intent(inout) :: popup |
| 61 | integer :: i |
| 62 | |
| 63 | if (allocated(popup%items)) then |
| 64 | do i = 1, popup%item_count |
| 65 | if (allocated(popup%items(i)%label)) deallocate(popup%items(i)%label) |
| 66 | if (allocated(popup%items(i)%kind)) deallocate(popup%items(i)%kind) |
| 67 | if (allocated(popup%items(i)%detail)) deallocate(popup%items(i)%detail) |
| 68 | if (allocated(popup%items(i)%insert_text)) deallocate(popup%items(i)%insert_text) |
| 69 | end do |
| 70 | deallocate(popup%items) |
| 71 | end if |
| 72 | |
| 73 | popup%item_count = 0 |
| 74 | popup%visible = .false. |
| 75 | end subroutine cleanup_completion_popup |
| 76 | |
| 77 | ! Handle LSP completion response and populate popup |
| 78 | subroutine handle_completion_response(popup, response) |
| 79 | type(completion_popup_t), intent(inout) :: popup |
| 80 | type(json_value_t), intent(in) :: response |
| 81 | type(json_value_t) :: items_array, item, text_edit |
| 82 | integer :: i, n_items, kind_num |
| 83 | character(len=:), allocatable :: label, kind_str, detail, insert_text |
| 84 | |
| 85 | call cleanup_completion_popup(popup) |
| 86 | |
| 87 | ! Check if response has items array |
| 88 | if (json_has_key(response, "items")) then |
| 89 | items_array = json_get_array(response, "items") |
| 90 | n_items = json_array_size(items_array) |
| 91 | |
| 92 | ! Limit to max visible items for now |
| 93 | n_items = min(n_items, MAX_VISIBLE_ITEMS * 2) |
| 94 | |
| 95 | if (n_items > 0) then |
| 96 | allocate(popup%items(n_items)) |
| 97 | popup%item_count = n_items |
| 98 | |
| 99 | do i = 1, n_items |
| 100 | item = json_get_array_element(items_array, i-1) |
| 101 | |
| 102 | ! Extract completion item fields |
| 103 | label = json_get_string(item, "label") |
| 104 | if (len_trim(label) > 0) then |
| 105 | popup%items(i)%label = label |
| 106 | else |
| 107 | popup%items(i)%label = "Item " // char(48 + mod(i-1, 10)) |
| 108 | end if |
| 109 | |
| 110 | ! Get completion kind (number mapping to type) |
| 111 | if (json_has_key(item, "kind")) then |
| 112 | kind_num = int(json_get_number(item, "kind")) |
| 113 | select case(kind_num) |
| 114 | case(1) |
| 115 | popup%items(i)%kind = "Text" |
| 116 | case(2) |
| 117 | popup%items(i)%kind = "Method" |
| 118 | case(3) |
| 119 | popup%items(i)%kind = "Function" |
| 120 | case(4) |
| 121 | popup%items(i)%kind = "Constructor" |
| 122 | case(5) |
| 123 | popup%items(i)%kind = "Field" |
| 124 | case(6) |
| 125 | popup%items(i)%kind = "Variable" |
| 126 | case(7) |
| 127 | popup%items(i)%kind = "Class" |
| 128 | case(8) |
| 129 | popup%items(i)%kind = "Interface" |
| 130 | case(9) |
| 131 | popup%items(i)%kind = "Module" |
| 132 | case(10) |
| 133 | popup%items(i)%kind = "Property" |
| 134 | case(14) |
| 135 | popup%items(i)%kind = "Keyword" |
| 136 | case default |
| 137 | popup%items(i)%kind = "" |
| 138 | end select |
| 139 | else |
| 140 | popup%items(i)%kind = "" |
| 141 | end if |
| 142 | |
| 143 | ! Get detail text if available |
| 144 | if (json_has_key(item, "detail")) then |
| 145 | detail = json_get_string(item, "detail") |
| 146 | popup%items(i)%detail = detail |
| 147 | else |
| 148 | popup%items(i)%detail = "" |
| 149 | end if |
| 150 | |
| 151 | ! Get insert text or fall back to label |
| 152 | if (json_has_key(item, "insertText")) then |
| 153 | insert_text = json_get_string(item, "insertText") |
| 154 | popup%items(i)%insert_text = insert_text |
| 155 | else |
| 156 | popup%items(i)%insert_text = popup%items(i)%label |
| 157 | end if |
| 158 | end do |
| 159 | |
| 160 | popup%selected_index = 1 |
| 161 | popup%scroll_offset = 0 |
| 162 | popup%height = min(popup%item_count, MAX_VISIBLE_ITEMS) + 2 ! +2 for borders |
| 163 | end if |
| 164 | end if |
| 165 | end subroutine handle_completion_response |
| 166 | |
| 167 | subroutine show_completion_popup(popup, row, col) |
| 168 | type(completion_popup_t), intent(inout) :: popup |
| 169 | integer, intent(in) :: row, col |
| 170 | integer :: i, display_row, start_idx, end_idx |
| 171 | character(len=256) :: line |
| 172 | |
| 173 | if (popup%item_count == 0) return |
| 174 | |
| 175 | popup%row = row |
| 176 | popup%col = col |
| 177 | popup%visible = .true. |
| 178 | |
| 179 | ! Calculate visible range |
| 180 | start_idx = popup%scroll_offset + 1 |
| 181 | end_idx = min(popup%scroll_offset + MAX_VISIBLE_ITEMS, popup%item_count) |
| 182 | |
| 183 | ! Draw top border |
| 184 | call terminal_move_cursor(popup%row, popup%col) |
| 185 | call terminal_write("┌" // repeat("─", popup%width - 2) // "┐") |
| 186 | |
| 187 | ! Draw items |
| 188 | display_row = popup%row + 1 |
| 189 | do i = start_idx, end_idx |
| 190 | call terminal_move_cursor(display_row, popup%col) |
| 191 | |
| 192 | ! Format item line |
| 193 | if (i == popup%selected_index) then |
| 194 | ! Highlight selected item with > marker |
| 195 | write(line, '(a,a)') "│>", adjustl(popup%items(i)%label) |
| 196 | call terminal_write(line(1:min(len_trim(line), popup%width - 1))) |
| 197 | call terminal_move_cursor(display_row, popup%col + popup%width - 1) |
| 198 | call terminal_write("│") |
| 199 | else |
| 200 | write(line, '(a,a)') "│ ", adjustl(popup%items(i)%label) |
| 201 | call terminal_write(line(1:min(len_trim(line), popup%width - 1))) |
| 202 | call terminal_move_cursor(display_row, popup%col + popup%width - 1) |
| 203 | call terminal_write("│") |
| 204 | end if |
| 205 | |
| 206 | display_row = display_row + 1 |
| 207 | end do |
| 208 | |
| 209 | ! Draw bottom border |
| 210 | call terminal_move_cursor(display_row, popup%col) |
| 211 | call terminal_write("└" // repeat("─", popup%width - 2) // "┘") |
| 212 | |
| 213 | end subroutine show_completion_popup |
| 214 | |
| 215 | subroutine hide_completion_popup(popup) |
| 216 | type(completion_popup_t), intent(inout) :: popup |
| 217 | popup%visible = .false. |
| 218 | end subroutine hide_completion_popup |
| 219 | |
| 220 | subroutine navigate_completion_up(popup) |
| 221 | type(completion_popup_t), intent(inout) :: popup |
| 222 | |
| 223 | if (.not. popup%visible .or. popup%item_count == 0) return |
| 224 | |
| 225 | if (popup%selected_index > 1) then |
| 226 | popup%selected_index = popup%selected_index - 1 |
| 227 | |
| 228 | ! Adjust scroll if needed |
| 229 | if (popup%selected_index <= popup%scroll_offset) then |
| 230 | popup%scroll_offset = popup%selected_index - 1 |
| 231 | end if |
| 232 | else |
| 233 | ! Wrap to bottom |
| 234 | popup%selected_index = popup%item_count |
| 235 | popup%scroll_offset = max(0, popup%item_count - MAX_VISIBLE_ITEMS) |
| 236 | end if |
| 237 | end subroutine navigate_completion_up |
| 238 | |
| 239 | subroutine navigate_completion_down(popup) |
| 240 | type(completion_popup_t), intent(inout) :: popup |
| 241 | |
| 242 | if (.not. popup%visible .or. popup%item_count == 0) return |
| 243 | |
| 244 | if (popup%selected_index < popup%item_count) then |
| 245 | popup%selected_index = popup%selected_index + 1 |
| 246 | |
| 247 | ! Adjust scroll if needed |
| 248 | if (popup%selected_index > popup%scroll_offset + MAX_VISIBLE_ITEMS) then |
| 249 | popup%scroll_offset = popup%selected_index - MAX_VISIBLE_ITEMS |
| 250 | end if |
| 251 | else |
| 252 | ! Wrap to top |
| 253 | popup%selected_index = 1 |
| 254 | popup%scroll_offset = 0 |
| 255 | end if |
| 256 | end subroutine navigate_completion_down |
| 257 | |
| 258 | function get_selected_completion(popup) result(text) |
| 259 | type(completion_popup_t), intent(in) :: popup |
| 260 | character(len=:), allocatable :: text |
| 261 | |
| 262 | if (popup%visible .and. popup%item_count > 0 .and. & |
| 263 | popup%selected_index > 0 .and. popup%selected_index <= popup%item_count) then |
| 264 | |
| 265 | if (allocated(popup%items(popup%selected_index)%insert_text)) then |
| 266 | text = popup%items(popup%selected_index)%insert_text |
| 267 | else |
| 268 | text = popup%items(popup%selected_index)%label |
| 269 | end if |
| 270 | else |
| 271 | text = "" |
| 272 | end if |
| 273 | end function get_selected_completion |
| 274 | |
| 275 | function is_completion_visible(popup) result(visible) |
| 276 | type(completion_popup_t), intent(in) :: popup |
| 277 | logical :: visible |
| 278 | |
| 279 | visible = popup%visible |
| 280 | end function is_completion_visible |
| 281 | |
| 282 | end module completion_popup_module |