fortrangoingonforty/facsimile / 47b313d

Browse files

feat: add LSP hover tooltip with Ctrl+H trigger

Co-Authored-By: mfwolffe <wolffemf@dukes.jmu.edu>
Authored by espadonne
SHA
47b313de316a3faedceefc325de5e0ac77627c98
Parents
caaf9cb
Tree
f47a7cd

5 changed files

StatusFile+-
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 \
8080
           src/lsp/lsp_server_manager_module.f90 \
8181
           src/lsp/lsp_client_module.f90 \
8282
           src/ui/completion_popup_module.f90 \
83
+          src/ui/hover_tooltip_module.f90 \
8384
           src/editor_state_module.f90 \
8485
           src/undo/undo_stack_module.f90 \
8586
           src/workspace/file_tree_module.f90 \
src/commands/command_handler_module.f90modified
@@ -28,6 +28,8 @@ module command_handler_module
2828
                                         handle_completion_response, navigate_completion_up, &
2929
                                         navigate_completion_down, get_selected_completion, &
3030
                                         is_completion_visible
31
+    use hover_tooltip_module, only: show_hover_tooltip, hide_hover_tooltip, &
32
+                                     handle_hover_response, is_hover_visible
3133
     implicit none
3234
     private
3335
 
@@ -130,6 +132,12 @@ contains
130132
                 return
131133
             end if
132134
 
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
+
133141
             ! ESC - Clear selections and return to single cursor mode
134142
             if (size(editor%cursors) > 1) then
135143
                 ! Keep only the active cursor
@@ -268,6 +276,11 @@ contains
268276
             call update_viewport(editor)
269277
 
270278
         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
+
271284
             if (size(editor%cursors) > 1) then
272285
                 ! Move all cursors
273286
                 do i = 1, size(editor%cursors)
@@ -282,6 +295,11 @@ contains
282295
             call update_viewport(editor)
283296
 
284297
         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
+
285303
             if (size(editor%cursors) > 1) then
286304
                 ! Move all cursors
287305
                 do i = 1, size(editor%cursors)
@@ -958,6 +976,13 @@ contains
958976
                             editor%tabs(editor%active_tab_index)%lsp_server_index, &
959977
                             editor%tabs(editor%active_tab_index)%filename, &
960978
                             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
961986
                     end block
962987
                 end if
963988
             end if
src/editor_state_module.f90modified
@@ -8,6 +8,8 @@ module editor_state_module
88
                                          request_completion, request_hover
99
     use completion_popup_module, only: completion_popup_t, init_completion_popup, &
1010
                                        cleanup_completion_popup
11
+    use hover_tooltip_module, only: hover_tooltip_t, init_hover_tooltip, &
12
+                                    cleanup_hover_tooltip
1113
     implicit none
1214
     private
1315
 
@@ -101,6 +103,7 @@ module editor_state_module
101103
         ! LSP support
102104
         type(lsp_manager_t) :: lsp_manager
103105
         type(completion_popup_t) :: completion_popup
106
+        type(hover_tooltip_t) :: hover_tooltip
104107
     end type editor_state_t
105108
 
106109
 contains
@@ -132,6 +135,9 @@ contains
132135
 
133136
         ! Initialize completion popup
134137
         call init_completion_popup(editor%completion_popup)
138
+
139
+        ! Initialize hover tooltip
140
+        call init_hover_tooltip(editor%hover_tooltip)
135141
     end subroutine init_editor
136142
 
137143
     subroutine cleanup_editor(editor)
@@ -155,6 +161,9 @@ contains
155161
 
156162
         ! Cleanup completion popup
157163
         call cleanup_completion_popup(editor%completion_popup)
164
+
165
+        ! Cleanup hover tooltip
166
+        call cleanup_hover_tooltip(editor%hover_tooltip)
158167
     end subroutine cleanup_editor
159168
 
160169
     ! Helper to cleanup a single tab
src/terminal/input_handler_module.f90modified
@@ -63,8 +63,8 @@ contains
6363
             key_str = 'tab'
6464
         case(10, 13)  ! Enter
6565
             key_str = 'enter'
66
-        case(8)  ! Ctrl-H (backspace)
67
-            key_str = 'backspace'
66
+        case(8)  ! Ctrl-H
67
+            key_str = 'ctrl-h'
6868
         case(26)  ! Ctrl-Z
6969
             key_str = 'ctrl-z'
7070
         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