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 | src/lsp/lsp_server_manager_module.f90 \ | 80 | src/lsp/lsp_server_manager_module.f90 \ |
| 81 | src/lsp/lsp_client_module.f90 \ | 81 | src/lsp/lsp_client_module.f90 \ |
| 82 | src/ui/completion_popup_module.f90 \ | 82 | src/ui/completion_popup_module.f90 \ |
| 83 | + src/ui/hover_tooltip_module.f90 \ | ||
| 83 | src/editor_state_module.f90 \ | 84 | src/editor_state_module.f90 \ |
| 84 | src/undo/undo_stack_module.f90 \ | 85 | src/undo/undo_stack_module.f90 \ |
| 85 | src/workspace/file_tree_module.f90 \ | 86 | src/workspace/file_tree_module.f90 \ |
src/commands/command_handler_module.f90modified@@ -28,6 +28,8 @@ module command_handler_module | |||
| 28 | handle_completion_response, navigate_completion_up, & | 28 | handle_completion_response, navigate_completion_up, & |
| 29 | navigate_completion_down, get_selected_completion, & | 29 | navigate_completion_down, get_selected_completion, & |
| 30 | is_completion_visible | 30 | is_completion_visible |
| 31 | + use hover_tooltip_module, only: show_hover_tooltip, hide_hover_tooltip, & | ||
| 32 | + handle_hover_response, is_hover_visible | ||
| 31 | implicit none | 33 | implicit none |
| 32 | private | 34 | private |
| 33 | 35 | ||
@@ -130,6 +132,12 @@ contains | |||
| 130 | return | 132 | return |
| 131 | end if | 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 | ! ESC - Clear selections and return to single cursor mode | 141 | ! ESC - Clear selections and return to single cursor mode |
| 134 | if (size(editor%cursors) > 1) then | 142 | if (size(editor%cursors) > 1) then |
| 135 | ! Keep only the active cursor | 143 | ! Keep only the active cursor |
@@ -268,6 +276,11 @@ contains | |||
| 268 | call update_viewport(editor) | 276 | call update_viewport(editor) |
| 269 | 277 | ||
| 270 | case('left') | 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 | if (size(editor%cursors) > 1) then | 284 | if (size(editor%cursors) > 1) then |
| 272 | ! Move all cursors | 285 | ! Move all cursors |
| 273 | do i = 1, size(editor%cursors) | 286 | do i = 1, size(editor%cursors) |
@@ -282,6 +295,11 @@ contains | |||
| 282 | call update_viewport(editor) | 295 | call update_viewport(editor) |
| 283 | 296 | ||
| 284 | case('right') | 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 | if (size(editor%cursors) > 1) then | 303 | if (size(editor%cursors) > 1) then |
| 286 | ! Move all cursors | 304 | ! Move all cursors |
| 287 | do i = 1, size(editor%cursors) | 305 | do i = 1, size(editor%cursors) |
@@ -958,6 +976,13 @@ contains | |||
| 958 | editor%tabs(editor%active_tab_index)%lsp_server_index, & | 976 | editor%tabs(editor%active_tab_index)%lsp_server_index, & |
| 959 | editor%tabs(editor%active_tab_index)%filename, & | 977 | editor%tabs(editor%active_tab_index)%filename, & |
| 960 | lsp_line, lsp_char) | 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 | end block | 986 | end block |
| 962 | end if | 987 | end if |
| 963 | end if | 988 | end if |
src/editor_state_module.f90modified@@ -8,6 +8,8 @@ module editor_state_module | |||
| 8 | request_completion, request_hover | 8 | request_completion, request_hover |
| 9 | use completion_popup_module, only: completion_popup_t, init_completion_popup, & | 9 | use completion_popup_module, only: completion_popup_t, init_completion_popup, & |
| 10 | cleanup_completion_popup | 10 | cleanup_completion_popup |
| 11 | + use hover_tooltip_module, only: hover_tooltip_t, init_hover_tooltip, & | ||
| 12 | + cleanup_hover_tooltip | ||
| 11 | implicit none | 13 | implicit none |
| 12 | private | 14 | private |
| 13 | 15 | ||
@@ -101,6 +103,7 @@ module editor_state_module | |||
| 101 | ! LSP support | 103 | ! LSP support |
| 102 | type(lsp_manager_t) :: lsp_manager | 104 | type(lsp_manager_t) :: lsp_manager |
| 103 | type(completion_popup_t) :: completion_popup | 105 | type(completion_popup_t) :: completion_popup |
| 106 | + type(hover_tooltip_t) :: hover_tooltip | ||
| 104 | end type editor_state_t | 107 | end type editor_state_t |
| 105 | 108 | ||
| 106 | contains | 109 | contains |
@@ -132,6 +135,9 @@ contains | |||
| 132 | 135 | ||
| 133 | ! Initialize completion popup | 136 | ! Initialize completion popup |
| 134 | call init_completion_popup(editor%completion_popup) | 137 | call init_completion_popup(editor%completion_popup) |
| 138 | + | ||
| 139 | + ! Initialize hover tooltip | ||
| 140 | + call init_hover_tooltip(editor%hover_tooltip) | ||
| 135 | end subroutine init_editor | 141 | end subroutine init_editor |
| 136 | 142 | ||
| 137 | subroutine cleanup_editor(editor) | 143 | subroutine cleanup_editor(editor) |
@@ -155,6 +161,9 @@ contains | |||
| 155 | 161 | ||
| 156 | ! Cleanup completion popup | 162 | ! Cleanup completion popup |
| 157 | call cleanup_completion_popup(editor%completion_popup) | 163 | call cleanup_completion_popup(editor%completion_popup) |
| 164 | + | ||
| 165 | + ! Cleanup hover tooltip | ||
| 166 | + call cleanup_hover_tooltip(editor%hover_tooltip) | ||
| 158 | end subroutine cleanup_editor | 167 | end subroutine cleanup_editor |
| 159 | 168 | ||
| 160 | ! Helper to cleanup a single tab | 169 | ! Helper to cleanup a single tab |
src/terminal/input_handler_module.f90modified@@ -63,8 +63,8 @@ contains | |||
| 63 | key_str = 'tab' | 63 | key_str = 'tab' |
| 64 | case(10, 13) ! Enter | 64 | case(10, 13) ! Enter |
| 65 | key_str = 'enter' | 65 | key_str = 'enter' |
| 66 | - case(8) ! Ctrl-H (backspace) | 66 | + case(8) ! Ctrl-H |
| 67 | - key_str = 'backspace' | 67 | + key_str = 'ctrl-h' |
| 68 | case(26) ! Ctrl-Z | 68 | case(26) ! Ctrl-Z |
| 69 | key_str = 'ctrl-z' | 69 | key_str = 'ctrl-z' |
| 70 | case(31) ! Ctrl-/ and Ctrl-? (both send ASCII 31) | 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 | ||