feat: add LSP hover tooltip with Ctrl+H trigger
Co-Authored-By: mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
47b313de316a3faedceefc325de5e0ac77627c98- Parents
-
caaf9cb - Tree
f47a7cd
47b313d
47b313de316a3faedceefc325de5e0ac77627c98caaf9cb
f47a7cd| Status | File | + | - |
|---|---|---|---|
| M |
Makefile
|
1 | 0 |
| M |
src/commands/command_handler_module.f90
|
25 | 0 |
| M |
src/editor_state_module.f90
|
9 | 0 |
| M |
src/terminal/input_handler_module.f90
|
2 | 2 |
| A |
src/ui/hover_tooltip_module.f90
|
168 | 0 |
Makefilemodified@@ -80,6 +80,7 @@ SOURCES = src/version_module.f90 \ | ||
| 80 | 80 | src/lsp/lsp_server_manager_module.f90 \ |
| 81 | 81 | src/lsp/lsp_client_module.f90 \ |
| 82 | 82 | src/ui/completion_popup_module.f90 \ |
| 83 | + src/ui/hover_tooltip_module.f90 \ | |
| 83 | 84 | src/editor_state_module.f90 \ |
| 84 | 85 | src/undo/undo_stack_module.f90 \ |
| 85 | 86 | src/workspace/file_tree_module.f90 \ |
src/commands/command_handler_module.f90modified@@ -28,6 +28,8 @@ module command_handler_module | ||
| 28 | 28 | handle_completion_response, navigate_completion_up, & |
| 29 | 29 | navigate_completion_down, get_selected_completion, & |
| 30 | 30 | is_completion_visible |
| 31 | + use hover_tooltip_module, only: show_hover_tooltip, hide_hover_tooltip, & | |
| 32 | + handle_hover_response, is_hover_visible | |
| 31 | 33 | implicit none |
| 32 | 34 | private |
| 33 | 35 | |
@@ -130,6 +132,12 @@ contains | ||
| 130 | 132 | return |
| 131 | 133 | end if |
| 132 | 134 | |
| 135 | + ! If hover tooltip is visible, hide it | |
| 136 | + if (is_hover_visible(editor%hover_tooltip)) then | |
| 137 | + call hide_hover_tooltip(editor%hover_tooltip) | |
| 138 | + return | |
| 139 | + end if | |
| 140 | + | |
| 133 | 141 | ! ESC - Clear selections and return to single cursor mode |
| 134 | 142 | if (size(editor%cursors) > 1) then |
| 135 | 143 | ! Keep only the active cursor |
@@ -268,6 +276,11 @@ contains | ||
| 268 | 276 | call update_viewport(editor) |
| 269 | 277 | |
| 270 | 278 | case('left') |
| 279 | + ! Hide hover tooltip on movement | |
| 280 | + if (is_hover_visible(editor%hover_tooltip)) then | |
| 281 | + call hide_hover_tooltip(editor%hover_tooltip) | |
| 282 | + end if | |
| 283 | + | |
| 271 | 284 | if (size(editor%cursors) > 1) then |
| 272 | 285 | ! Move all cursors |
| 273 | 286 | do i = 1, size(editor%cursors) |
@@ -282,6 +295,11 @@ contains | ||
| 282 | 295 | call update_viewport(editor) |
| 283 | 296 | |
| 284 | 297 | case('right') |
| 298 | + ! Hide hover tooltip on movement | |
| 299 | + if (is_hover_visible(editor%hover_tooltip)) then | |
| 300 | + call hide_hover_tooltip(editor%hover_tooltip) | |
| 301 | + end if | |
| 302 | + | |
| 285 | 303 | if (size(editor%cursors) > 1) then |
| 286 | 304 | ! Move all cursors |
| 287 | 305 | do i = 1, size(editor%cursors) |
@@ -958,6 +976,13 @@ contains | ||
| 958 | 976 | editor%tabs(editor%active_tab_index)%lsp_server_index, & |
| 959 | 977 | editor%tabs(editor%active_tab_index)%filename, & |
| 960 | 978 | lsp_line, lsp_char) |
| 979 | + | |
| 980 | + if (request_id > 0) then | |
| 981 | + ! Show tooltip at cursor position (will populate when response arrives) | |
| 982 | + call show_hover_tooltip(editor%hover_tooltip, & | |
| 983 | + editor%cursors(editor%active_cursor)%line - editor%viewport_line + 2, & | |
| 984 | + editor%cursors(editor%active_cursor)%column - editor%viewport_column + 1) | |
| 985 | + end if | |
| 961 | 986 | end block |
| 962 | 987 | end if |
| 963 | 988 | end if |
src/editor_state_module.f90modified@@ -8,6 +8,8 @@ module editor_state_module | ||
| 8 | 8 | request_completion, request_hover |
| 9 | 9 | use completion_popup_module, only: completion_popup_t, init_completion_popup, & |
| 10 | 10 | cleanup_completion_popup |
| 11 | + use hover_tooltip_module, only: hover_tooltip_t, init_hover_tooltip, & | |
| 12 | + cleanup_hover_tooltip | |
| 11 | 13 | implicit none |
| 12 | 14 | private |
| 13 | 15 | |
@@ -101,6 +103,7 @@ module editor_state_module | ||
| 101 | 103 | ! LSP support |
| 102 | 104 | type(lsp_manager_t) :: lsp_manager |
| 103 | 105 | type(completion_popup_t) :: completion_popup |
| 106 | + type(hover_tooltip_t) :: hover_tooltip | |
| 104 | 107 | end type editor_state_t |
| 105 | 108 | |
| 106 | 109 | contains |
@@ -132,6 +135,9 @@ contains | ||
| 132 | 135 | |
| 133 | 136 | ! Initialize completion popup |
| 134 | 137 | call init_completion_popup(editor%completion_popup) |
| 138 | + | |
| 139 | + ! Initialize hover tooltip | |
| 140 | + call init_hover_tooltip(editor%hover_tooltip) | |
| 135 | 141 | end subroutine init_editor |
| 136 | 142 | |
| 137 | 143 | subroutine cleanup_editor(editor) |
@@ -155,6 +161,9 @@ contains | ||
| 155 | 161 | |
| 156 | 162 | ! Cleanup completion popup |
| 157 | 163 | call cleanup_completion_popup(editor%completion_popup) |
| 164 | + | |
| 165 | + ! Cleanup hover tooltip | |
| 166 | + call cleanup_hover_tooltip(editor%hover_tooltip) | |
| 158 | 167 | end subroutine cleanup_editor |
| 159 | 168 | |
| 160 | 169 | ! Helper to cleanup a single tab |
src/terminal/input_handler_module.f90modified@@ -63,8 +63,8 @@ contains | ||
| 63 | 63 | key_str = 'tab' |
| 64 | 64 | case(10, 13) ! Enter |
| 65 | 65 | key_str = 'enter' |
| 66 | - case(8) ! Ctrl-H (backspace) | |
| 67 | - key_str = 'backspace' | |
| 66 | + case(8) ! Ctrl-H | |
| 67 | + key_str = 'ctrl-h' | |
| 68 | 68 | case(26) ! Ctrl-Z |
| 69 | 69 | key_str = 'ctrl-z' |
| 70 | 70 | case(31) ! Ctrl-/ and Ctrl-? (both send ASCII 31) |
src/ui/hover_tooltip_module.f90added@@ -0,0 +1,168 @@ | ||
| 1 | +module hover_tooltip_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 | + implicit none | |
| 7 | + private | |
| 8 | + | |
| 9 | + public :: hover_tooltip_t | |
| 10 | + public :: init_hover_tooltip, cleanup_hover_tooltip | |
| 11 | + public :: show_hover_tooltip, hide_hover_tooltip | |
| 12 | + public :: handle_hover_response | |
| 13 | + public :: is_hover_visible | |
| 14 | + | |
| 15 | + integer, parameter :: MAX_WIDTH = 60 | |
| 16 | + integer, parameter :: MAX_HEIGHT = 10 | |
| 17 | + | |
| 18 | + type :: hover_tooltip_t | |
| 19 | + character(len=:), allocatable :: content | |
| 20 | + logical :: visible = .false. | |
| 21 | + | |
| 22 | + ! Position on screen | |
| 23 | + integer :: row = 0 | |
| 24 | + integer :: col = 0 | |
| 25 | + integer :: width = 0 | |
| 26 | + integer :: height = 0 | |
| 27 | + end type hover_tooltip_t | |
| 28 | + | |
| 29 | +contains | |
| 30 | + | |
| 31 | + subroutine init_hover_tooltip(tooltip) | |
| 32 | + type(hover_tooltip_t), intent(out) :: tooltip | |
| 33 | + | |
| 34 | + tooltip%visible = .false. | |
| 35 | + tooltip%row = 0 | |
| 36 | + tooltip%col = 0 | |
| 37 | + tooltip%width = 0 | |
| 38 | + tooltip%height = 0 | |
| 39 | + | |
| 40 | + if (allocated(tooltip%content)) deallocate(tooltip%content) | |
| 41 | + end subroutine init_hover_tooltip | |
| 42 | + | |
| 43 | + subroutine cleanup_hover_tooltip(tooltip) | |
| 44 | + type(hover_tooltip_t), intent(inout) :: tooltip | |
| 45 | + | |
| 46 | + if (allocated(tooltip%content)) deallocate(tooltip%content) | |
| 47 | + tooltip%visible = .false. | |
| 48 | + end subroutine cleanup_hover_tooltip | |
| 49 | + | |
| 50 | + ! Handle LSP hover response and populate tooltip | |
| 51 | + subroutine handle_hover_response(tooltip, response) | |
| 52 | + type(hover_tooltip_t), intent(inout) :: tooltip | |
| 53 | + type(json_value_t), intent(in) :: response | |
| 54 | + type(json_value_t) :: contents | |
| 55 | + character(len=:), allocatable :: hover_text | |
| 56 | + | |
| 57 | + call cleanup_hover_tooltip(tooltip) | |
| 58 | + | |
| 59 | + ! Check if response has contents | |
| 60 | + if (json_has_key(response, "contents")) then | |
| 61 | + contents = json_get_object(response, "contents") | |
| 62 | + | |
| 63 | + ! Try to get value from MarkupContent or MarkedString | |
| 64 | + if (json_has_key(contents, "value")) then | |
| 65 | + hover_text = json_get_string(contents, "value") | |
| 66 | + else | |
| 67 | + ! Might be a plain string | |
| 68 | + hover_text = json_get_string(response, "contents") | |
| 69 | + end if | |
| 70 | + | |
| 71 | + if (len_trim(hover_text) > 0) then | |
| 72 | + tooltip%content = hover_text | |
| 73 | + call calculate_tooltip_dimensions(tooltip) | |
| 74 | + end if | |
| 75 | + end if | |
| 76 | + end subroutine handle_hover_response | |
| 77 | + | |
| 78 | + subroutine calculate_tooltip_dimensions(tooltip) | |
| 79 | + type(hover_tooltip_t), intent(inout) :: tooltip | |
| 80 | + integer :: content_len, lines, i, line_start, line_len, max_line_len | |
| 81 | + | |
| 82 | + if (.not. allocated(tooltip%content)) return | |
| 83 | + | |
| 84 | + content_len = len(tooltip%content) | |
| 85 | + lines = 1 | |
| 86 | + max_line_len = 0 | |
| 87 | + line_start = 1 | |
| 88 | + | |
| 89 | + ! Count lines and find max line length | |
| 90 | + do i = 1, content_len | |
| 91 | + if (tooltip%content(i:i) == char(10)) then ! newline | |
| 92 | + line_len = i - line_start | |
| 93 | + if (line_len > max_line_len) max_line_len = line_len | |
| 94 | + lines = lines + 1 | |
| 95 | + line_start = i + 1 | |
| 96 | + end if | |
| 97 | + end do | |
| 98 | + | |
| 99 | + ! Check last line | |
| 100 | + line_len = content_len - line_start + 1 | |
| 101 | + if (line_len > max_line_len) max_line_len = line_len | |
| 102 | + | |
| 103 | + tooltip%width = min(max_line_len + 4, MAX_WIDTH) ! +4 for borders and padding | |
| 104 | + tooltip%height = min(lines + 2, MAX_HEIGHT) ! +2 for borders | |
| 105 | + end subroutine calculate_tooltip_dimensions | |
| 106 | + | |
| 107 | + subroutine show_hover_tooltip(tooltip, row, col) | |
| 108 | + type(hover_tooltip_t), intent(inout) :: tooltip | |
| 109 | + integer, intent(in) :: row, col | |
| 110 | + integer :: display_row, i, line_start, line_end | |
| 111 | + character(len=256) :: line | |
| 112 | + | |
| 113 | + if (.not. allocated(tooltip%content)) return | |
| 114 | + if (len_trim(tooltip%content) == 0) return | |
| 115 | + | |
| 116 | + tooltip%row = row | |
| 117 | + tooltip%col = col | |
| 118 | + tooltip%visible = .true. | |
| 119 | + | |
| 120 | + ! Draw top border | |
| 121 | + call terminal_move_cursor(tooltip%row, tooltip%col) | |
| 122 | + call terminal_write("┌" // repeat("─", tooltip%width - 2) // "┐") | |
| 123 | + | |
| 124 | + ! Draw content lines | |
| 125 | + display_row = tooltip%row + 1 | |
| 126 | + line_start = 1 | |
| 127 | + | |
| 128 | + do i = 1, len(tooltip%content) | |
| 129 | + if (tooltip%content(i:i) == char(10) .or. i == len(tooltip%content)) then | |
| 130 | + if (i == len(tooltip%content) .and. tooltip%content(i:i) /= char(10)) then | |
| 131 | + line_end = i | |
| 132 | + else | |
| 133 | + line_end = i - 1 | |
| 134 | + end if | |
| 135 | + | |
| 136 | + call terminal_move_cursor(display_row, tooltip%col) | |
| 137 | + write(line, '(a,a,a)') "│ ", & | |
| 138 | + tooltip%content(line_start:line_end), " " | |
| 139 | + call terminal_write(line(1:min(len_trim(line), tooltip%width - 1))) | |
| 140 | + call terminal_move_cursor(display_row, tooltip%col + tooltip%width - 1) | |
| 141 | + call terminal_write("│") | |
| 142 | + | |
| 143 | + display_row = display_row + 1 | |
| 144 | + line_start = i + 1 | |
| 145 | + | |
| 146 | + if (display_row - tooltip%row >= tooltip%height - 1) exit | |
| 147 | + end if | |
| 148 | + end do | |
| 149 | + | |
| 150 | + ! Draw bottom border | |
| 151 | + call terminal_move_cursor(display_row, tooltip%col) | |
| 152 | + call terminal_write("└" // repeat("─", tooltip%width - 2) // "┘") | |
| 153 | + | |
| 154 | + end subroutine show_hover_tooltip | |
| 155 | + | |
| 156 | + subroutine hide_hover_tooltip(tooltip) | |
| 157 | + type(hover_tooltip_t), intent(inout) :: tooltip | |
| 158 | + tooltip%visible = .false. | |
| 159 | + end subroutine hide_hover_tooltip | |
| 160 | + | |
| 161 | + function is_hover_visible(tooltip) result(visible) | |
| 162 | + type(hover_tooltip_t), intent(in) :: tooltip | |
| 163 | + logical :: visible | |
| 164 | + | |
| 165 | + visible = tooltip%visible | |
| 166 | + end function is_hover_visible | |
| 167 | + | |
| 168 | +end module hover_tooltip_module | |