Fortran · 20967 bytes Raw Blame History
1 module workspace_symbols_panel_module
2 use iso_fortran_env, only: int32
3 use terminal_io_module
4 implicit none
5 private
6
7 public :: workspace_symbols_panel_t, workspace_symbol_t
8 public :: init_workspace_symbols_panel, cleanup_workspace_symbols_panel
9 public :: show_workspace_symbols_panel, hide_workspace_symbols_panel
10 public :: is_workspace_symbols_panel_visible, workspace_symbols_panel_handle_key
11 public :: set_workspace_symbols, get_selected_symbol
12 public :: render_workspace_symbols_panel, get_search_query
13 public :: add_search_char, delete_search_char, clear_search
14
15 integer, parameter :: MAX_SYMBOLS = 1000
16
17 type :: workspace_symbol_t
18 character(len=:), allocatable :: name
19 character(len=:), allocatable :: kind_name ! "Function", "Class", etc.
20 character(len=:), allocatable :: container_name
21 character(len=:), allocatable :: file_uri
22 character(len=:), allocatable :: file_path ! Extracted from URI
23 integer :: line = 0
24 integer :: column = 0
25 integer :: kind = 0
26 integer :: score = 0 ! For fuzzy matching
27 end type workspace_symbol_t
28
29 type :: workspace_symbols_panel_t
30 logical :: visible = .false.
31 integer :: panel_width = 60
32 integer :: panel_start_col = 1
33 integer :: max_visible = 20
34 type(workspace_symbol_t), allocatable :: all_symbols(:)
35 integer :: num_symbols = 0
36 type(workspace_symbol_t), allocatable :: filtered_symbols(:)
37 integer :: num_filtered = 0
38 integer :: selected_index = 1
39 integer :: scroll_offset = 0
40 character(len=256) :: search_query = ''
41 integer :: search_pos = 0
42 logical :: needs_lsp_query = .false. ! Flag to trigger LSP request
43 end type workspace_symbols_panel_t
44
45 contains
46
47 subroutine init_workspace_symbols_panel(panel)
48 type(workspace_symbols_panel_t), intent(out) :: panel
49
50 panel%visible = .false.
51 panel%num_symbols = 0
52 panel%num_filtered = 0
53 panel%selected_index = 1
54 panel%scroll_offset = 0
55 panel%search_query = ''
56 panel%search_pos = 0
57 panel%needs_lsp_query = .false.
58
59 allocate(panel%all_symbols(MAX_SYMBOLS))
60 allocate(panel%filtered_symbols(MAX_SYMBOLS))
61 end subroutine init_workspace_symbols_panel
62
63 subroutine cleanup_workspace_symbols_panel(panel)
64 type(workspace_symbols_panel_t), intent(inout) :: panel
65 integer :: i
66
67 if (allocated(panel%all_symbols)) then
68 do i = 1, panel%num_symbols
69 if (allocated(panel%all_symbols(i)%name)) deallocate(panel%all_symbols(i)%name)
70 if (allocated(panel%all_symbols(i)%kind_name)) deallocate(panel%all_symbols(i)%kind_name)
71 if (allocated(panel%all_symbols(i)%container_name)) deallocate(panel%all_symbols(i)%container_name)
72 if (allocated(panel%all_symbols(i)%file_uri)) deallocate(panel%all_symbols(i)%file_uri)
73 if (allocated(panel%all_symbols(i)%file_path)) deallocate(panel%all_symbols(i)%file_path)
74 end do
75 deallocate(panel%all_symbols)
76 end if
77
78 if (allocated(panel%filtered_symbols)) deallocate(panel%filtered_symbols)
79 end subroutine cleanup_workspace_symbols_panel
80
81 subroutine show_workspace_symbols_panel(panel, screen_width, screen_height)
82 type(workspace_symbols_panel_t), intent(inout) :: panel
83 integer, intent(in) :: screen_width, screen_height
84
85 panel%visible = .true.
86 panel%panel_width = min(70, screen_width / 2)
87 panel%panel_start_col = screen_width - panel%panel_width + 1
88 panel%max_visible = screen_height - 5 ! Room for header, search, hints
89 panel%search_query = ''
90 panel%search_pos = 0
91 panel%selected_index = 1
92 panel%scroll_offset = 0
93 panel%needs_lsp_query = .true. ! Request initial symbols
94
95 ! Initially show all symbols
96 call filter_symbols(panel)
97 end subroutine show_workspace_symbols_panel
98
99 subroutine hide_workspace_symbols_panel(panel)
100 type(workspace_symbols_panel_t), intent(inout) :: panel
101 panel%visible = .false.
102 end subroutine hide_workspace_symbols_panel
103
104 function is_workspace_symbols_panel_visible(panel) result(visible)
105 type(workspace_symbols_panel_t), intent(in) :: panel
106 logical :: visible
107 visible = panel%visible
108 end function is_workspace_symbols_panel_visible
109
110 function get_search_query(panel) result(query)
111 type(workspace_symbols_panel_t), intent(in) :: panel
112 character(len=:), allocatable :: query
113 if (panel%search_pos > 0) then
114 query = trim(panel%search_query(1:panel%search_pos))
115 else
116 query = ''
117 end if
118 end function get_search_query
119
120 subroutine add_search_char(panel, ch)
121 type(workspace_symbols_panel_t), intent(inout) :: panel
122 character, intent(in) :: ch
123
124 if (panel%search_pos < 255) then
125 panel%search_pos = panel%search_pos + 1
126 panel%search_query(panel%search_pos:panel%search_pos) = ch
127 panel%needs_lsp_query = .true.
128 call filter_symbols(panel)
129 end if
130 end subroutine add_search_char
131
132 subroutine delete_search_char(panel)
133 type(workspace_symbols_panel_t), intent(inout) :: panel
134
135 if (panel%search_pos > 0) then
136 panel%search_query(panel%search_pos:panel%search_pos) = ' '
137 panel%search_pos = panel%search_pos - 1
138 panel%needs_lsp_query = .true.
139 call filter_symbols(panel)
140 end if
141 end subroutine delete_search_char
142
143 subroutine clear_search(panel)
144 type(workspace_symbols_panel_t), intent(inout) :: panel
145 panel%search_query = ''
146 panel%search_pos = 0
147 panel%needs_lsp_query = .true.
148 call filter_symbols(panel)
149 end subroutine clear_search
150
151 subroutine set_workspace_symbols(panel, symbols, count)
152 type(workspace_symbols_panel_t), intent(inout) :: panel
153 type(workspace_symbol_t), intent(in) :: symbols(:)
154 integer, intent(in) :: count
155 integer :: i, copy_count
156
157 copy_count = min(count, MAX_SYMBOLS)
158 panel%num_symbols = copy_count
159
160 do i = 1, copy_count
161 ! Deep copy each symbol
162 if (allocated(panel%all_symbols(i)%name)) deallocate(panel%all_symbols(i)%name)
163 if (allocated(symbols(i)%name)) then
164 allocate(character(len=len(symbols(i)%name)) :: panel%all_symbols(i)%name)
165 panel%all_symbols(i)%name = symbols(i)%name
166 end if
167
168 if (allocated(panel%all_symbols(i)%kind_name)) deallocate(panel%all_symbols(i)%kind_name)
169 if (allocated(symbols(i)%kind_name)) then
170 allocate(character(len=len(symbols(i)%kind_name)) :: panel%all_symbols(i)%kind_name)
171 panel%all_symbols(i)%kind_name = symbols(i)%kind_name
172 end if
173
174 if (allocated(panel%all_symbols(i)%container_name)) deallocate(panel%all_symbols(i)%container_name)
175 if (allocated(symbols(i)%container_name)) then
176 allocate(character(len=len(symbols(i)%container_name)) :: panel%all_symbols(i)%container_name)
177 panel%all_symbols(i)%container_name = symbols(i)%container_name
178 end if
179
180 if (allocated(panel%all_symbols(i)%file_uri)) deallocate(panel%all_symbols(i)%file_uri)
181 if (allocated(symbols(i)%file_uri)) then
182 allocate(character(len=len(symbols(i)%file_uri)) :: panel%all_symbols(i)%file_uri)
183 panel%all_symbols(i)%file_uri = symbols(i)%file_uri
184 end if
185
186 if (allocated(panel%all_symbols(i)%file_path)) deallocate(panel%all_symbols(i)%file_path)
187 if (allocated(symbols(i)%file_path)) then
188 allocate(character(len=len(symbols(i)%file_path)) :: panel%all_symbols(i)%file_path)
189 panel%all_symbols(i)%file_path = symbols(i)%file_path
190 end if
191
192 panel%all_symbols(i)%line = symbols(i)%line
193 panel%all_symbols(i)%column = symbols(i)%column
194 panel%all_symbols(i)%kind = symbols(i)%kind
195 panel%all_symbols(i)%score = symbols(i)%score
196 end do
197
198 ! Refilter with current query
199 call filter_symbols(panel)
200 end subroutine set_workspace_symbols
201
202 subroutine filter_symbols(panel)
203 type(workspace_symbols_panel_t), intent(inout) :: panel
204 character(len=:), allocatable :: query
205 integer :: i, score
206
207 if (panel%search_pos > 0) then
208 query = trim(panel%search_query(1:panel%search_pos))
209 else
210 query = ''
211 end if
212
213 panel%num_filtered = 0
214
215 do i = 1, panel%num_symbols
216 if (allocated(panel%all_symbols(i)%name)) then
217 score = fuzzy_match_score(panel%all_symbols(i)%name, query)
218 if (score > 0) then
219 panel%num_filtered = panel%num_filtered + 1
220 panel%filtered_symbols(panel%num_filtered) = panel%all_symbols(i)
221 panel%filtered_symbols(panel%num_filtered)%score = score
222 end if
223 end if
224 end do
225
226 ! Sort by score (simple bubble sort for now)
227 call sort_symbols_by_score(panel%filtered_symbols, panel%num_filtered)
228
229 ! Reset selection
230 panel%selected_index = 1
231 panel%scroll_offset = 0
232 end subroutine filter_symbols
233
234 function fuzzy_match_score(text, pattern) result(score)
235 character(len=*), intent(in) :: text, pattern
236 integer :: score
237 integer :: i, j, text_len, pattern_len
238 integer :: consecutive_matches
239 character :: text_lower, pattern_lower
240
241 score = 0
242 text_len = len_trim(text)
243 pattern_len = len_trim(pattern)
244
245 ! Empty pattern matches everything
246 if (pattern_len == 0) then
247 score = 100
248 return
249 end if
250
251 j = 1
252 consecutive_matches = 0
253
254 do i = 1, text_len
255 if (j > pattern_len) exit
256
257 ! Case-insensitive comparison
258 text_lower = to_lower(text(i:i))
259 pattern_lower = to_lower(pattern(j:j))
260
261 if (text_lower == pattern_lower) then
262 score = score + 10
263 consecutive_matches = consecutive_matches + 1
264
265 ! Bonus for consecutive matches
266 if (consecutive_matches > 1) then
267 score = score + 5
268 end if
269
270 ! Bonus for matching at word start
271 if (i == 1 .or. text(i-1:i-1) == ' ' .or. text(i-1:i-1) == '_') then
272 score = score + 15
273 end if
274
275 j = j + 1
276 else
277 consecutive_matches = 0
278 end if
279 end do
280
281 ! Only match if all pattern characters were found
282 if (j <= pattern_len) then
283 score = 0
284 end if
285 end function fuzzy_match_score
286
287 function to_lower(ch) result(lower)
288 character, intent(in) :: ch
289 character :: lower
290 integer :: code
291
292 code = iachar(ch)
293 if (code >= iachar('A') .and. code <= iachar('Z')) then
294 lower = achar(code + 32)
295 else
296 lower = ch
297 end if
298 end function to_lower
299
300 subroutine sort_symbols_by_score(symbols, count)
301 type(workspace_symbol_t), intent(inout) :: symbols(:)
302 integer, intent(in) :: count
303 type(workspace_symbol_t) :: temp
304 integer :: i, j
305
306 ! Bubble sort (good enough for small lists)
307 do i = 1, count - 1
308 do j = i + 1, count
309 if (symbols(j)%score > symbols(i)%score) then
310 temp = symbols(i)
311 symbols(i) = symbols(j)
312 symbols(j) = temp
313 end if
314 end do
315 end do
316 end subroutine sort_symbols_by_score
317
318 function get_selected_symbol(panel) result(sym)
319 type(workspace_symbols_panel_t), intent(in) :: panel
320 type(workspace_symbol_t) :: sym
321
322 if (panel%num_filtered > 0 .and. panel%selected_index <= panel%num_filtered) then
323 sym = panel%filtered_symbols(panel%selected_index)
324 end if
325 end function get_selected_symbol
326
327 function workspace_symbols_panel_handle_key(panel, key) result(handled)
328 type(workspace_symbols_panel_t), intent(inout) :: panel
329 character(len=*), intent(in) :: key
330 logical :: handled
331
332 handled = .false.
333 if (.not. panel%visible) return
334
335 select case(trim(key))
336 case('j', 'down', 'ctrl-n')
337 ! Always handle to clamp at boundary
338 handled = .true.
339 if (panel%selected_index < panel%num_filtered) then
340 panel%selected_index = panel%selected_index + 1
341 if (panel%selected_index > panel%scroll_offset + panel%max_visible) then
342 panel%scroll_offset = panel%selected_index - panel%max_visible
343 end if
344 end if
345
346 case('k', 'up', 'ctrl-p')
347 ! Always handle to clamp at boundary
348 handled = .true.
349 if (panel%selected_index > 1) then
350 panel%selected_index = panel%selected_index - 1
351 if (panel%selected_index <= panel%scroll_offset) then
352 panel%scroll_offset = max(0, panel%selected_index - 1)
353 end if
354 end if
355
356 case('ctrl-u')
357 ! Clear search
358 call clear_search(panel)
359 handled = .true.
360
361 case('esc', 'escape')
362 call hide_workspace_symbols_panel(panel)
363 handled = .true.
364
365 case('enter')
366 ! Signal to jump - handled by command_handler
367 handled = .true.
368
369 case('backspace', 'ctrl-h')
370 call delete_search_char(panel)
371 handled = .true.
372
373 case default
374 ! Check if it's a printable character for search
375 if (len_trim(key) == 1) then
376 if (iachar(key(1:1)) >= 32 .and. iachar(key(1:1)) < 127) then
377 call add_search_char(panel, key(1:1))
378 handled = .true.
379 end if
380 end if
381 end select
382 end function workspace_symbols_panel_handle_key
383
384 subroutine render_workspace_symbols_panel(panel, screen_height)
385 type(workspace_symbols_panel_t), intent(in) :: panel
386 integer, intent(in) :: screen_height
387 integer :: row, i, start_idx, end_idx
388 character(len=256) :: line, file_info
389 character(len=1), parameter :: ESC = char(27)
390
391 if (.not. panel%visible) return
392
393 ! Draw title bar with background color
394 row = 1
395 call terminal_move_cursor(row, panel%panel_start_col)
396 call terminal_write(ESC // '[48;5;237m' // ESC // '[1m') ! Dark bg, bold
397 line = " Workspace Symbols"
398 if (panel%num_filtered > 0) then
399 write(line, '(A,I0,A)') trim(line) // " (", panel%num_filtered, ")"
400 end if
401 call terminal_write(line(1:min(len_trim(line), panel%panel_width)))
402 call terminal_write(repeat(" ", max(0, panel%panel_width - len_trim(line))))
403 call terminal_write(ESC // '[0m')
404
405 ! Draw search input
406 row = 2
407 call terminal_move_cursor(row, panel%panel_start_col)
408 call terminal_write(ESC // '[48;5;236m') ! Slightly lighter for input
409 if (panel%search_pos > 0) then
410 line = " > " // trim(panel%search_query(1:panel%search_pos))
411 else
412 line = " > " // ESC // '[90m' // "(type to filter)" // ESC // '[0m' // ESC // '[48;5;236m'
413 end if
414 call terminal_write(line(1:min(len_trim(line), panel%panel_width)))
415 call terminal_write(repeat(" ", max(0, panel%panel_width - len_trim(line))))
416 call terminal_write(ESC // '[0m')
417
418 ! Draw separator
419 row = 3
420 call terminal_move_cursor(row, panel%panel_start_col)
421 call terminal_write(ESC // '[48;5;237m' // repeat("-", panel%panel_width) // ESC // '[0m')
422
423 ! Calculate visible range
424 start_idx = panel%scroll_offset + 1
425 end_idx = min(panel%scroll_offset + panel%max_visible, panel%num_filtered)
426
427 ! Render symbols
428 if (panel%num_filtered > 0) then
429 do i = start_idx, end_idx
430 row = row + 1
431 call terminal_move_cursor(row, panel%panel_start_col)
432
433 ! Background color based on selection
434 if (i == panel%selected_index) then
435 call terminal_write(ESC // '[48;5;240m') ! Highlight
436 else
437 call terminal_write(ESC // '[48;5;235m') ! Normal
438 end if
439
440 ! Build display line: icon + name + file:line
441 line = " "
442
443 ! Add kind indicator
444 if (allocated(panel%filtered_symbols(i)%kind_name)) then
445 line = trim(line) // "[" // trim(panel%filtered_symbols(i)%kind_name(1:min(3, len_trim(panel%filtered_symbols(i)%kind_name)))) // "] "
446 else
447 line = trim(line) // " "
448 end if
449
450 ! Add symbol name
451 if (allocated(panel%filtered_symbols(i)%name)) then
452 line = trim(line) // trim(panel%filtered_symbols(i)%name)
453 end if
454
455 ! Add file info on selected item
456 if (i == panel%selected_index) then
457 if (allocated(panel%filtered_symbols(i)%file_path)) then
458 write(file_info, '(A,A,A,I0)') " (", &
459 trim(get_basename(panel%filtered_symbols(i)%file_path)), &
460 ":", panel%filtered_symbols(i)%line
461 file_info = trim(file_info) // ")"
462 if (len_trim(line) + len_trim(file_info) < panel%panel_width - 1) then
463 line = trim(line) // ESC // '[90m' // trim(file_info) // ESC // '[0m' // ESC // '[48;5;240m'
464 end if
465 end if
466 end if
467
468 ! Write line and pad
469 call terminal_write(line(1:min(len_trim(line), panel%panel_width)))
470 call terminal_write(repeat(" ", max(0, panel%panel_width - len_trim(line))))
471 call terminal_write(ESC // '[0m')
472 end do
473
474 ! Fill empty rows
475 do while (row < screen_height - 1)
476 row = row + 1
477 call terminal_move_cursor(row, panel%panel_start_col)
478 call terminal_write(ESC // '[48;5;235m' // repeat(" ", panel%panel_width) // ESC // '[0m')
479 end do
480 else
481 ! No symbols message
482 row = row + 1
483 call terminal_move_cursor(row, panel%panel_start_col)
484 call terminal_write(ESC // '[48;5;235m' // ESC // '[90m')
485 if (panel%search_pos == 0) then
486 line = " Type to search..."
487 else if (panel%num_symbols == 0) then
488 line = " Searching..."
489 else
490 line = " No matching symbols"
491 end if
492 call terminal_write(line(1:min(len_trim(line), panel%panel_width)))
493 call terminal_write(repeat(" ", max(0, panel%panel_width - len_trim(line))))
494 call terminal_write(ESC // '[0m')
495
496 do while (row < screen_height - 1)
497 row = row + 1
498 call terminal_move_cursor(row, panel%panel_start_col)
499 call terminal_write(ESC // '[48;5;235m' // repeat(" ", panel%panel_width) // ESC // '[0m')
500 end do
501 end if
502
503 ! Draw hint bar at bottom
504 call terminal_move_cursor(screen_height, panel%panel_start_col)
505 call terminal_write(ESC // '[48;5;237m' // ESC // '[90m')
506 line = " Enter:open Esc:close Ctrl-U:clear"
507 call terminal_write(line(1:min(len_trim(line), panel%panel_width)))
508 call terminal_write(repeat(" ", max(0, panel%panel_width - len_trim(line))))
509 call terminal_write(ESC // '[0m')
510 end subroutine render_workspace_symbols_panel
511
512 function get_basename(path) result(basename)
513 character(len=*), intent(in) :: path
514 character(len=256) :: basename
515 integer :: i, last_slash
516
517 last_slash = 0
518 do i = len_trim(path), 1, -1
519 if (path(i:i) == '/') then
520 last_slash = i
521 exit
522 end if
523 end do
524
525 if (last_slash > 0 .and. last_slash < len_trim(path)) then
526 basename = path(last_slash+1:len_trim(path))
527 else
528 basename = path
529 end if
530 end function get_basename
531
532 end module workspace_symbols_panel_module
533