Fortran · 57592 bytes Raw Blame History
1 module lsp_server_manager_module
2 ! Manages multiple language servers
3 use iso_fortran_env, only: int32, int64, real64, error_unit
4 use iso_c_binding
5 use json_module
6 use lsp_protocol_module
7 implicit none
8 private
9
10 public :: lsp_server_t
11 public :: lsp_manager_t
12 public :: init_lsp_manager, cleanup_lsp_manager, set_lsp_workspace_root
13 public :: get_or_start_server, stop_server
14 public :: send_request, send_notification
15 public :: process_server_messages
16 public :: register_callback
17 public :: get_language_for_file, start_lsp_for_file
18 public :: start_all_lsp_servers_for_file ! NEW: multi-server support
19 public :: get_server_with_capability ! NEW: capability-based routing
20 public :: notify_file_opened, notify_file_changed, notify_file_saved, notify_file_closed
21 public :: request_completion, request_hover, request_definition, request_references, request_code_actions
22 public :: request_document_symbols, request_signature_help, request_formatting, request_rename
23 public :: request_workspace_symbols
24 public :: set_diagnostics_handler
25 public :: filename_to_uri
26
27 ! Capability constants for routing requests to the right server
28 integer, parameter, public :: CAP_COMPLETION = 1
29 integer, parameter, public :: CAP_DEFINITION = 2
30 integer, parameter, public :: CAP_REFERENCES = 3
31 integer, parameter, public :: CAP_RENAME = 4
32 integer, parameter, public :: CAP_CODE_ACTIONS = 5
33 integer, parameter, public :: CAP_FORMATTING = 6
34 integer, parameter, public :: CAP_DIAGNOSTICS = 7
35 integer, parameter, public :: CAP_HOVER = 8
36 integer, parameter, public :: CAP_DOCUMENT_SYMBOLS = 9
37 integer, parameter, public :: CAP_WORKSPACE_SYMBOLS = 10
38 integer, parameter, public :: NUM_CAPABILITIES = 10
39
40 ! Language server configuration
41 type :: server_config_t
42 character(len=:), allocatable :: language
43 character(len=:), allocatable :: name ! Server name (e.g., "pyright", "ruff")
44 character(len=:), allocatable :: command
45 character(len=:), allocatable :: file_patterns
46 logical :: capabilities(10) = .false. ! Which features this server provides
47 end type server_config_t
48
49 ! Language server instance
50 type :: lsp_server_t
51 type(c_ptr) :: handle = c_null_ptr
52 character(len=:), allocatable :: language
53 character(len=:), allocatable :: name ! Server name (e.g., "pyright", "ruff")
54 character(len=:), allocatable :: command
55 character(len=:), allocatable :: root_path
56 logical :: initialized = .false.
57 logical :: initializing = .false.
58 integer :: process_id = -1
59 integer :: config_index = 0 ! Index into configs array
60 integer :: init_request_id = 0 ! Request ID of initialize request
61
62 ! Capabilities
63 logical :: supports_completion = .false.
64 logical :: supports_hover = .false.
65 logical :: supports_definition = .false.
66 logical :: supports_references = .false.
67 logical :: supports_code_actions = .false.
68 logical :: supports_document_symbols = .false.
69 logical :: supports_workspace_symbols = .false.
70 logical :: supports_formatting = .false.
71 logical :: supports_rename = .false.
72
73 ! Request tracking
74 integer, allocatable :: pending_requests(:)
75 integer :: num_pending = 0
76
77 ! Pending didOpen notifications (queued before initialization)
78 type(pending_didopen_t), allocatable :: pending_didopens(:)
79 integer :: num_pending_didopens = 0
80
81 ! Message buffer
82 character(len=:), allocatable :: read_buffer
83 end type lsp_server_t
84
85 ! Pending didOpen notification
86 type :: pending_didopen_t
87 character(len=:), allocatable :: filename
88 character(len=:), allocatable :: content
89 end type pending_didopen_t
90
91 ! Callback type for responses
92 abstract interface
93 subroutine response_callback(request_id, response)
94 use lsp_protocol_module, only: lsp_message_t
95 integer, intent(in) :: request_id
96 type(lsp_message_t), intent(in) :: response
97 end subroutine response_callback
98 end interface
99
100 ! Callback type for diagnostics notifications (includes server_index for multi-LSP)
101 abstract interface
102 subroutine diagnostics_callback(notification, server_index)
103 use lsp_protocol_module, only: lsp_message_t
104 type(lsp_message_t), intent(in) :: notification
105 integer, intent(in) :: server_index
106 end subroutine diagnostics_callback
107 end interface
108
109 ! Request callback entry
110 type :: callback_entry_t
111 integer :: request_id
112 procedure(response_callback), pointer, nopass :: callback => null()
113 end type callback_entry_t
114
115 ! Main LSP manager
116 type :: lsp_manager_t
117 type(lsp_server_t), allocatable :: servers(:)
118 integer :: num_servers = 0
119 type(server_config_t), allocatable :: configs(:)
120 integer :: num_configs = 0
121 type(callback_entry_t), allocatable :: callbacks(:)
122 integer :: num_callbacks = 0
123 procedure(diagnostics_callback), pointer, nopass :: diagnostics_handler => null()
124 character(len=512) :: workspace_root = '.' ! LSP workspace root (cwd by default)
125 end type lsp_manager_t
126
127 ! C interfaces
128 interface
129 subroutine lsp_start_server_f(command, command_len, handle) bind(c, name='lsp_start_server_f')
130 use iso_c_binding
131 character(c_char), intent(in) :: command(*)
132 integer(c_int), value :: command_len
133 type(c_ptr), intent(out) :: handle
134 end subroutine lsp_start_server_f
135
136 subroutine lsp_stop_server_f(handle) bind(c, name='lsp_stop_server_f')
137 use iso_c_binding
138 type(c_ptr), intent(inout) :: handle
139 end subroutine lsp_stop_server_f
140
141 function lsp_send_message_f(handle, message, message_len) bind(c, name='lsp_send_message_f')
142 use iso_c_binding
143 integer(c_int) :: lsp_send_message_f
144 type(c_ptr), intent(in) :: handle
145 character(c_char), intent(in) :: message(*)
146 integer(c_int), value :: message_len
147 end function lsp_send_message_f
148
149 function lsp_read_message_f(handle, buffer, buffer_len) bind(c, name='lsp_read_message_f')
150 use iso_c_binding
151 integer(c_int) :: lsp_read_message_f
152 type(c_ptr), intent(in) :: handle
153 character(c_char), intent(out) :: buffer(*)
154 integer(c_int), value :: buffer_len
155 end function lsp_read_message_f
156
157 function lsp_is_running_f(handle) bind(c, name='lsp_is_running_f')
158 use iso_c_binding
159 integer(c_int) :: lsp_is_running_f
160 type(c_ptr), intent(in) :: handle
161 end function lsp_is_running_f
162
163 function lsp_get_pid_f(handle) bind(c, name='lsp_get_pid_f')
164 use iso_c_binding
165 integer(c_int) :: lsp_get_pid_f
166 type(c_ptr), intent(in) :: handle
167 end function lsp_get_pid_f
168 end interface
169
170 contains
171
172 subroutine init_lsp_manager(manager, workspace_root)
173 type(lsp_manager_t), intent(out) :: manager
174 character(len=*), intent(in), optional :: workspace_root
175 character(len=512) :: cwd
176 integer :: status
177
178 allocate(manager%servers(0))
179 allocate(manager%configs(0))
180 allocate(manager%callbacks(0))
181
182 ! Set workspace root (default to cwd if not provided)
183 if (present(workspace_root) .and. len_trim(workspace_root) > 0) then
184 manager%workspace_root = trim(workspace_root)
185 else
186 call getcwd(cwd, status)
187 if (status == 0) then
188 manager%workspace_root = trim(cwd)
189 else
190 manager%workspace_root = '.'
191 end if
192 end if
193
194 ! Load default server configurations
195 call load_default_configs(manager)
196 end subroutine init_lsp_manager
197
198 ! Set the workspace root for LSP servers (call before opening files)
199 subroutine set_lsp_workspace_root(manager, workspace_root)
200 type(lsp_manager_t), intent(inout) :: manager
201 character(len=*), intent(in) :: workspace_root
202
203 manager%workspace_root = trim(workspace_root)
204 end subroutine set_lsp_workspace_root
205
206 subroutine cleanup_lsp_manager(manager)
207 type(lsp_manager_t), intent(inout) :: manager
208 integer :: i
209
210 ! Stop all servers
211 do i = 1, manager%num_servers
212 call stop_server(manager%servers(i))
213 end do
214
215 if (allocated(manager%servers)) deallocate(manager%servers)
216 if (allocated(manager%configs)) deallocate(manager%configs)
217 if (allocated(manager%callbacks)) deallocate(manager%callbacks)
218
219 manager%num_servers = 0
220 manager%num_configs = 0
221 manager%num_callbacks = 0
222 end subroutine cleanup_lsp_manager
223
224 subroutine load_default_configs(manager)
225 type(lsp_manager_t), intent(inout) :: manager
226 logical :: caps(NUM_CAPABILITIES)
227
228 ! Python - Pyright for semantic features (rename, definition, references, completion, hover)
229 caps = .false.
230 caps(CAP_COMPLETION) = .true.
231 caps(CAP_DEFINITION) = .true.
232 caps(CAP_REFERENCES) = .true.
233 caps(CAP_RENAME) = .true.
234 caps(CAP_HOVER) = .true.
235 caps(CAP_DIAGNOSTICS) = .true.
236 caps(CAP_DOCUMENT_SYMBOLS) = .true.
237 caps(CAP_WORKSPACE_SYMBOLS) = .true.
238 call add_config(manager, "python", "pyright", "pyright-langserver --stdio", "*.py", caps)
239
240 ! Python - Ruff for linting and code actions
241 caps = .false.
242 caps(CAP_CODE_ACTIONS) = .true.
243 caps(CAP_FORMATTING) = .true.
244 caps(CAP_DIAGNOSTICS) = .true.
245 call add_config(manager, "python", "ruff", "ruff server", "*.py", caps)
246
247 ! Rust
248 caps = .false.
249 caps(CAP_COMPLETION) = .true.
250 caps(CAP_DEFINITION) = .true.
251 caps(CAP_REFERENCES) = .true.
252 caps(CAP_RENAME) = .true.
253 caps(CAP_CODE_ACTIONS) = .true.
254 caps(CAP_HOVER) = .true.
255 caps(CAP_DIAGNOSTICS) = .true.
256 caps(CAP_DOCUMENT_SYMBOLS) = .true.
257 caps(CAP_FORMATTING) = .true.
258 call add_config(manager, "rust", "rust-analyzer", "rust-analyzer", "*.rs", caps)
259
260 ! C/C++
261 caps = .false.
262 caps(CAP_COMPLETION) = .true.
263 caps(CAP_DEFINITION) = .true.
264 caps(CAP_REFERENCES) = .true.
265 caps(CAP_RENAME) = .true.
266 caps(CAP_CODE_ACTIONS) = .true.
267 caps(CAP_HOVER) = .true.
268 caps(CAP_DIAGNOSTICS) = .true.
269 caps(CAP_DOCUMENT_SYMBOLS) = .true.
270 caps(CAP_FORMATTING) = .true.
271 call add_config(manager, "c", "clangd", "clangd", "*.c,*.h", caps)
272 call add_config(manager, "cpp", "clangd", "clangd", "*.cpp,*.cc,*.cxx,*.hpp,*.hxx", caps)
273
274 ! Go
275 caps = .false.
276 caps(CAP_COMPLETION) = .true.
277 caps(CAP_DEFINITION) = .true.
278 caps(CAP_REFERENCES) = .true.
279 caps(CAP_RENAME) = .true.
280 caps(CAP_CODE_ACTIONS) = .true.
281 caps(CAP_HOVER) = .true.
282 caps(CAP_DIAGNOSTICS) = .true.
283 caps(CAP_DOCUMENT_SYMBOLS) = .true.
284 caps(CAP_FORMATTING) = .true.
285 call add_config(manager, "go", "gopls", "gopls", "*.go", caps)
286
287 ! TypeScript/JavaScript
288 caps = .false.
289 caps(CAP_COMPLETION) = .true.
290 caps(CAP_DEFINITION) = .true.
291 caps(CAP_REFERENCES) = .true.
292 caps(CAP_RENAME) = .true.
293 caps(CAP_CODE_ACTIONS) = .true.
294 caps(CAP_HOVER) = .true.
295 caps(CAP_DIAGNOSTICS) = .true.
296 caps(CAP_DOCUMENT_SYMBOLS) = .true.
297 caps(CAP_FORMATTING) = .true.
298 call add_config(manager, "typescript", "ts-server", "typescript-language-server --stdio", "*.ts,*.tsx", caps)
299 call add_config(manager, "javascript", "ts-server", "typescript-language-server --stdio", "*.js,*.jsx", caps)
300
301 ! Fortran
302 caps = .false.
303 caps(CAP_COMPLETION) = .true.
304 caps(CAP_DEFINITION) = .true.
305 caps(CAP_REFERENCES) = .true.
306 caps(CAP_HOVER) = .true.
307 caps(CAP_DIAGNOSTICS) = .true.
308 caps(CAP_DOCUMENT_SYMBOLS) = .true.
309 caps(CAP_CODE_ACTIONS) = .true. ! fortls has limited code action support
310 call add_config(manager, "fortran", "fortls", "fortls", "*.f90,*.f95,*.f03,*.f08", caps)
311
312 ! TODO: Load from config file
313 end subroutine load_default_configs
314
315 ! Convert filename to absolute file:// URI
316 function filename_to_uri(filename) result(uri)
317 character(len=*), intent(in) :: filename
318 character(len=:), allocatable :: uri
319 character(len=4096) :: cwd
320 integer :: status
321
322 if (len_trim(filename) > 0 .and. filename(1:1) == '/') then
323 ! Already absolute path
324 uri = "file://" // trim(filename)
325 else
326 ! Relative path - prepend current directory
327 call getcwd(cwd, status)
328 if (status == 0) then
329 uri = "file://" // trim(cwd) // "/" // trim(filename)
330 else
331 ! Fallback if getcwd fails
332 uri = "file://" // trim(filename)
333 end if
334 end if
335 end function filename_to_uri
336
337 subroutine add_config(manager, language, name, command, patterns, capabilities)
338 type(lsp_manager_t), intent(inout) :: manager
339 character(len=*), intent(in) :: language, name, command, patterns
340 logical, intent(in) :: capabilities(NUM_CAPABILITIES)
341 type(server_config_t), allocatable :: new_configs(:)
342
343 allocate(new_configs(manager%num_configs + 1))
344 if (manager%num_configs > 0) then
345 new_configs(1:manager%num_configs) = manager%configs
346 end if
347
348 new_configs(manager%num_configs + 1)%language = language
349 new_configs(manager%num_configs + 1)%name = name
350 new_configs(manager%num_configs + 1)%command = command
351 new_configs(manager%num_configs + 1)%file_patterns = patterns
352 new_configs(manager%num_configs + 1)%capabilities = capabilities
353
354 deallocate(manager%configs)
355 manager%configs = new_configs
356 manager%num_configs = manager%num_configs + 1
357 end subroutine add_config
358
359 function get_or_start_server(manager, language, root_path) result(server_index)
360 type(lsp_manager_t), intent(inout) :: manager
361 character(len=*), intent(in) :: language, root_path
362 integer :: server_index
363 integer :: i
364
365 ! Check if server already exists
366 do i = 1, manager%num_servers
367 if (manager%servers(i)%language == language .and. &
368 manager%servers(i)%root_path == root_path) then
369 server_index = i
370 return
371 end if
372 end do
373
374 ! Start new server
375 server_index = start_new_server(manager, language, root_path)
376 end function get_or_start_server
377
378 function start_new_server(manager, language, root_path) result(server_index)
379 type(lsp_manager_t), intent(inout) :: manager
380 character(len=*), intent(in) :: language, root_path
381 integer :: server_index
382 type(lsp_server_t), allocatable :: new_servers(:)
383 character(len=:), allocatable :: command
384 integer :: i
385
386 server_index = 0
387
388 ! Find command for language
389 command = ""
390 do i = 1, manager%num_configs
391 if (manager%configs(i)%language == language) then
392 command = manager%configs(i)%command
393 exit
394 end if
395 end do
396
397 if (command == "") then
398 write(error_unit, '(a,a)') "No LSP server configured for language: ", language
399 return
400 end if
401
402 ! Expand server array
403 allocate(new_servers(manager%num_servers + 1))
404 if (manager%num_servers > 0) then
405 new_servers(1:manager%num_servers) = manager%servers
406 end if
407
408 ! Initialize new server
409 new_servers(manager%num_servers + 1)%language = language
410 new_servers(manager%num_servers + 1)%command = command
411 new_servers(manager%num_servers + 1)%root_path = root_path
412 new_servers(manager%num_servers + 1)%initialized = .false.
413 new_servers(manager%num_servers + 1)%initializing = .false.
414 allocate(new_servers(manager%num_servers + 1)%pending_requests(100))
415 new_servers(manager%num_servers + 1)%num_pending = 0
416 new_servers(manager%num_servers + 1)%read_buffer = ""
417
418 ! Start the server process
419 call lsp_start_server_f(command//c_null_char, len(command), &
420 new_servers(manager%num_servers + 1)%handle)
421
422 if (c_associated(new_servers(manager%num_servers + 1)%handle)) then
423 new_servers(manager%num_servers + 1)%process_id = &
424 lsp_get_pid_f(new_servers(manager%num_servers + 1)%handle)
425
426 deallocate(manager%servers)
427 manager%servers = new_servers
428 manager%num_servers = manager%num_servers + 1
429 server_index = manager%num_servers
430
431 ! Send initialization request
432 call initialize_server(manager%servers(server_index))
433 else
434 write(error_unit, '(a,a)') "Failed to start LSP server: ", command
435 end if
436 end function start_new_server
437
438 subroutine initialize_server(server)
439 type(lsp_server_t), intent(inout) :: server
440 type(lsp_message_t) :: msg
441 character(len=:), allocatable :: json_msg
442
443 if (server%initializing .or. server%initialized) return
444
445 server%initializing = .true.
446
447 ! Create initialization request
448 msg = create_initialize_request(server%process_id, server%root_path, "fac")
449
450 ! Store the request ID so we can match the response
451 server%init_request_id = msg%id
452
453 ! Send it
454 json_msg = format_json_rpc(msg)
455 call send_raw_message(server, json_msg)
456
457 ! Track the request
458 call track_request(server, msg%id)
459 end subroutine initialize_server
460
461 subroutine stop_server(server)
462 type(lsp_server_t), intent(inout) :: server
463
464 if (c_associated(server%handle)) then
465 ! Send shutdown request
466 ! TODO: Implement proper shutdown sequence
467
468 call lsp_stop_server_f(server%handle)
469 server%handle = c_null_ptr
470 end if
471
472 server%initialized = .false.
473 server%initializing = .false.
474 server%process_id = -1
475 end subroutine stop_server
476
477 subroutine send_request(manager, server_index, msg, callback)
478 type(lsp_manager_t), intent(inout) :: manager
479 integer, intent(in) :: server_index
480 type(lsp_message_t), intent(in) :: msg
481 procedure(response_callback), optional :: callback
482 character(len=:), allocatable :: json_msg
483
484 if (server_index < 1 .or. server_index > manager%num_servers) return
485
486 if (.not. manager%servers(server_index)%initialized .and. &
487 .not. manager%servers(server_index)%initializing) return
488
489 json_msg = format_json_rpc(msg)
490 call send_raw_message(manager%servers(server_index), json_msg)
491
492 ! Track request and register callback
493 call track_request(manager%servers(server_index), msg%id)
494
495 if (present(callback)) then
496 call register_callback(manager, msg%id, callback)
497 end if
498 end subroutine send_request
499
500 subroutine send_notification(server, msg)
501 type(lsp_server_t), intent(inout) :: server
502 type(lsp_message_t), intent(in) :: msg
503 character(len=:), allocatable :: json_msg
504
505 if (.not. server%initialized .and. .not. server%initializing) return
506
507 json_msg = format_json_rpc(msg)
508 call send_raw_message(server, json_msg)
509 end subroutine send_notification
510
511 subroutine send_raw_message(server, message)
512 type(lsp_server_t), intent(inout) :: server
513 character(len=*), intent(in) :: message
514 integer :: result
515
516 if (.not. c_associated(server%handle)) return
517
518 result = lsp_send_message_f(server%handle, message//c_null_char, len(message))
519
520 if (result < 0) then
521 write(error_unit, '(a)') "Failed to send message to LSP server"
522 end if
523 end subroutine send_raw_message
524
525 subroutine process_server_messages(manager)
526 type(lsp_manager_t), intent(inout) :: manager
527 integer :: i
528
529 do i = 1, manager%num_servers
530 if (c_associated(manager%servers(i)%handle)) then
531 call process_server_output(manager, manager%servers(i))
532 end if
533 end do
534 end subroutine process_server_messages
535
536 subroutine process_server_output(manager, server)
537 type(lsp_manager_t), intent(inout) :: manager
538 type(lsp_server_t), intent(inout) :: server
539 character(len=65536) :: buffer
540 integer :: bytes_read
541
542 ! Read from server
543 bytes_read = lsp_read_message_f(server%handle, buffer, len(buffer))
544
545 if (bytes_read > 0) then
546 ! Append to buffer
547 server%read_buffer = server%read_buffer // buffer(1:bytes_read)
548
549 ! Try to parse complete messages
550 call parse_messages(manager, server)
551 end if
552 end subroutine process_server_output
553
554 subroutine parse_messages(manager, server)
555 type(lsp_manager_t), intent(inout) :: manager
556 type(lsp_server_t), intent(inout) :: server
557 integer :: content_length, header_end, message_end
558 character(len=:), allocatable :: message
559 type(lsp_message_t) :: msg
560
561 do
562 ! Look for Content-Length header
563 content_length = extract_content_length(server%read_buffer)
564 if (content_length <= 0) exit
565
566 ! Find where content starts
567 header_end = index(server%read_buffer, char(13)//char(10)//char(13)//char(10))
568 if (header_end <= 0) exit
569
570 ! Check if we have the complete message
571 message_end = header_end + 3 + content_length
572 if (len(server%read_buffer) < message_end) exit
573
574 ! Extract and parse message
575 message = server%read_buffer(1:message_end)
576 msg = parse_lsp_message(message)
577
578 ! Handle the message
579 call handle_message(manager, server, msg)
580
581 ! Remove from buffer
582 server%read_buffer = server%read_buffer(message_end+1:)
583 end do
584 end subroutine parse_messages
585
586 function extract_content_length(buffer) result(length)
587 character(len=*), intent(in) :: buffer
588 integer :: length
589 integer :: pos, end_pos
590 character(len=32) :: len_str
591
592 length = -1
593
594 pos = index(buffer, "Content-Length:")
595 if (pos <= 0) return
596
597 pos = pos + 15 ! Skip "Content-Length:"
598
599 ! Find end of line
600 end_pos = index(buffer(pos:), char(13))
601 if (end_pos <= 0) return
602
603 len_str = adjustl(buffer(pos:pos+end_pos-2))
604 read(len_str, '(i10)', iostat=pos) length
605 if (pos /= 0) length = -1
606 end function extract_content_length
607
608 subroutine handle_message(manager, server, msg)
609 type(lsp_manager_t), intent(inout) :: manager
610 type(lsp_server_t), intent(inout) :: server
611 type(lsp_message_t), intent(in) :: msg
612
613 if (msg%is_response) then
614 call handle_response(manager, server, msg)
615 else if (msg%is_notification) then
616 call handle_notification(manager, server, msg)
617 else if (msg%is_request) then
618 call handle_request(manager, server, msg)
619 end if
620 end subroutine handle_message
621
622 subroutine handle_response(manager, server, msg)
623 type(lsp_manager_t), intent(inout) :: manager
624 type(lsp_server_t), intent(inout) :: server
625 type(lsp_message_t), intent(in) :: msg
626 integer :: i
627
628 ! Remove from pending requests
629 call untrack_request(server, msg%id)
630
631 ! Special handling for initialization response
632 if (server%initializing .and. msg%id == server%init_request_id) then
633 call handle_initialize_response(server, msg)
634 return
635 end if
636
637 ! Find and call registered callback
638 do i = 1, manager%num_callbacks
639 if (manager%callbacks(i)%request_id == msg%id) then
640 if (associated(manager%callbacks(i)%callback)) then
641 call manager%callbacks(i)%callback(msg%id, msg)
642 end if
643 ! Remove callback
644 call remove_callback(manager, i)
645 exit
646 end if
647 end do
648 end subroutine handle_response
649
650 subroutine handle_initialize_response(server, msg)
651 type(lsp_server_t), intent(inout) :: server
652 type(lsp_message_t), intent(in) :: msg
653 type(json_value_t) :: capabilities
654 type(lsp_message_t) :: initialized_msg
655
656 ! Parse server capabilities
657 capabilities = json_get_object(msg%result, "capabilities")
658
659 ! Check what the server supports
660 if (json_has_key(capabilities, "completionProvider")) then
661 server%supports_completion = .true.
662 end if
663
664 if (json_has_key(capabilities, "hoverProvider")) then
665 server%supports_hover = .true.
666 end if
667
668 if (json_has_key(capabilities, "definitionProvider")) then
669 server%supports_definition = .true.
670 end if
671
672 if (json_has_key(capabilities, "referencesProvider")) then
673 server%supports_references = .true.
674 end if
675
676 if (json_has_key(capabilities, "codeActionProvider")) then
677 server%supports_code_actions = .true.
678 end if
679
680 if (json_has_key(capabilities, "documentSymbolProvider")) then
681 server%supports_document_symbols = .true.
682 end if
683
684 if (json_has_key(capabilities, "workspaceSymbolProvider")) then
685 server%supports_workspace_symbols = .true.
686 end if
687
688 if (json_has_key(capabilities, "documentFormattingProvider")) then
689 server%supports_formatting = .true.
690 end if
691
692 if (json_has_key(capabilities, "renameProvider")) then
693 server%supports_rename = .true.
694 end if
695
696 if (json_has_key(capabilities, "codeActionProvider")) then
697 server%supports_code_actions = .true.
698 end if
699
700 ! Send initialized notification
701 initialized_msg = create_initialized_notification()
702 call send_notification(server, initialized_msg)
703
704 server%initialized = .true.
705 server%initializing = .false.
706
707 ! Send all queued didOpen notifications
708 if (allocated(server%pending_didopens) .and. server%num_pending_didopens > 0) then
709 block
710 integer :: i
711 type(lsp_message_t) :: didopen_msg
712 character(len=:), allocatable :: lang, file_uri
713
714 do i = 1, server%num_pending_didopens
715 if (allocated(server%pending_didopens(i)%filename) .and. &
716 allocated(server%pending_didopens(i)%content)) then
717
718 lang = get_language_for_file(server%pending_didopens(i)%filename)
719 file_uri = filename_to_uri(server%pending_didopens(i)%filename)
720 didopen_msg = create_did_open_notification( &
721 file_uri, lang, 1, &
722 server%pending_didopens(i)%content)
723 call send_notification(server, didopen_msg)
724 end if
725 end do
726
727 ! Clear the queue
728 deallocate(server%pending_didopens)
729 server%num_pending_didopens = 0
730 end block
731 end if
732 end subroutine handle_initialize_response
733
734 subroutine handle_notification(manager, server, msg)
735 type(lsp_manager_t), intent(inout) :: manager
736 type(lsp_server_t), intent(inout) :: server
737 type(lsp_message_t), intent(in) :: msg
738 integer :: srv_idx, i
739
740 ! Find the server index for this server
741 srv_idx = 0
742 do i = 1, manager%num_servers
743 if (allocated(manager%servers(i)%name) .and. allocated(server%name)) then
744 if (manager%servers(i)%name == server%name .and. &
745 manager%servers(i)%root_path == server%root_path) then
746 srv_idx = i
747 exit
748 end if
749 end if
750 end do
751
752 select case(msg%method)
753 case("textDocument/publishDiagnostics")
754 ! Forward to diagnostics handler if set (with server index)
755 if (associated(manager%diagnostics_handler)) then
756 call manager%diagnostics_handler(msg, srv_idx)
757 end if
758 case("window/showMessage")
759 ! TODO: Show message to user
760 case("window/logMessage")
761 ! TODO: Log message
762 case default
763 ! Unknown notification
764 end select
765 end subroutine handle_notification
766
767 subroutine handle_request(manager, server, msg)
768 type(lsp_manager_t), intent(inout) :: manager
769 type(lsp_server_t), intent(inout) :: server
770 type(lsp_message_t), intent(in) :: msg
771
772 ! Servers can send requests too (like workspace/configuration)
773 ! TODO: Handle server requests
774 ! Silence unused argument warnings (stub for future implementation)
775 if (.false.) print *, manager%num_servers, server%initialized, msg%id
776 end subroutine handle_request
777
778 subroutine track_request(server, request_id)
779 type(lsp_server_t), intent(inout) :: server
780 integer, intent(in) :: request_id
781
782 if (server%num_pending < size(server%pending_requests)) then
783 server%num_pending = server%num_pending + 1
784 server%pending_requests(server%num_pending) = request_id
785 end if
786 end subroutine track_request
787
788 subroutine untrack_request(server, request_id)
789 type(lsp_server_t), intent(inout) :: server
790 integer, intent(in) :: request_id
791 integer :: i, j
792
793 do i = 1, server%num_pending
794 if (server%pending_requests(i) == request_id) then
795 ! Shift remaining requests
796 do j = i, server%num_pending - 1
797 server%pending_requests(j) = server%pending_requests(j + 1)
798 end do
799 server%num_pending = server%num_pending - 1
800 exit
801 end if
802 end do
803 end subroutine untrack_request
804
805 subroutine register_callback(manager, request_id, callback)
806 type(lsp_manager_t), intent(inout) :: manager
807 integer, intent(in) :: request_id
808 procedure(response_callback) :: callback
809 type(callback_entry_t), allocatable :: new_callbacks(:)
810
811 allocate(new_callbacks(manager%num_callbacks + 1))
812
813 if (manager%num_callbacks > 0) then
814 new_callbacks(1:manager%num_callbacks) = manager%callbacks
815 end if
816
817 new_callbacks(manager%num_callbacks + 1)%request_id = request_id
818 new_callbacks(manager%num_callbacks + 1)%callback => callback
819
820 if (allocated(manager%callbacks)) deallocate(manager%callbacks)
821 allocate(manager%callbacks(size(new_callbacks)))
822 manager%callbacks = new_callbacks
823 manager%num_callbacks = manager%num_callbacks + 1
824 end subroutine register_callback
825
826 subroutine remove_callback(manager, index)
827 type(lsp_manager_t), intent(inout) :: manager
828 integer, intent(in) :: index
829 integer :: i
830
831 if (index < 1 .or. index > manager%num_callbacks) return
832
833 ! Shift remaining callbacks
834 do i = index, manager%num_callbacks - 1
835 manager%callbacks(i) = manager%callbacks(i + 1)
836 end do
837
838 manager%num_callbacks = manager%num_callbacks - 1
839 end subroutine remove_callback
840
841 ! Helper to get language from filename
842 function get_language_for_file(filename) result(language)
843 character(len=*), intent(in) :: filename
844 character(len=:), allocatable :: language
845 integer :: dot_pos
846
847 ! Find last dot in filename
848 dot_pos = index(filename, '.', back=.true.)
849 if (dot_pos == 0) then
850 language = ""
851 return
852 end if
853
854 ! Match extension to language
855 select case(filename(dot_pos:))
856 case('.py')
857 language = "python"
858 case('.rs')
859 language = "rust"
860 case('.c', '.h')
861 language = "c"
862 case('.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.C', '.H')
863 language = "cpp"
864 case('.go')
865 language = "go"
866 case('.ts', '.tsx')
867 language = "typescript"
868 case('.js', '.jsx')
869 language = "javascript"
870 case('.f90', '.f95', '.f03', '.f08', '.F90', '.F95', '.F03', '.F08')
871 language = "fortran"
872 case('.java')
873 language = "java"
874 case('.rb')
875 language = "ruby"
876 case('.lua')
877 language = "lua"
878 case default
879 language = ""
880 end select
881 end function get_language_for_file
882
883 ! Start LSP server for a file if needed
884 function start_lsp_for_file(manager, filename) result(server_index)
885 type(lsp_manager_t), intent(inout) :: manager
886 character(len=*), intent(in) :: filename
887 integer :: server_index
888 character(len=:), allocatable :: language
889
890 server_index = 0
891
892 ! Get language from file extension
893 language = get_language_for_file(filename)
894 if (language == "") return
895
896 ! Use manager's workspace_root (set during init, defaults to cwd)
897 server_index = get_or_start_server(manager, language, trim(manager%workspace_root))
898 end function start_lsp_for_file
899
900 ! Start ALL LSP servers that match a file (multi-server support)
901 subroutine start_all_lsp_servers_for_file(manager, filename, server_indices, num_servers)
902 type(lsp_manager_t), intent(inout) :: manager
903 character(len=*), intent(in) :: filename
904 integer, allocatable, intent(out) :: server_indices(:)
905 integer, intent(out) :: num_servers
906 character(len=:), allocatable :: language
907 integer :: i, idx
908 integer :: temp_indices(20) ! Max 20 servers per file
909
910 num_servers = 0
911 allocate(server_indices(0))
912
913 ! Get language from file extension
914 language = get_language_for_file(filename)
915 if (language == "") return
916
917 ! Use manager's workspace_root (set during init, defaults to cwd)
918 ! Find ALL configs that match this language and start servers
919 do i = 1, manager%num_configs
920 if (manager%configs(i)%language == language) then
921 ! Start or get server for this config
922 idx = get_or_start_server_by_config(manager, i, trim(manager%workspace_root))
923 if (idx > 0 .and. num_servers < 20) then
924 num_servers = num_servers + 1
925 temp_indices(num_servers) = idx
926 end if
927 end if
928 end do
929
930 ! Copy to output array
931 if (num_servers > 0) then
932 deallocate(server_indices)
933 allocate(server_indices(num_servers))
934 server_indices = temp_indices(1:num_servers)
935 end if
936 end subroutine start_all_lsp_servers_for_file
937
938 ! Get or start server for a specific config index
939 function get_or_start_server_by_config(manager, config_index, root_path) result(server_index)
940 type(lsp_manager_t), intent(inout) :: manager
941 integer, intent(in) :: config_index
942 character(len=*), intent(in) :: root_path
943 integer :: server_index
944 integer :: i
945
946 server_index = 0
947 if (config_index < 1 .or. config_index > manager%num_configs) return
948
949 ! Check if server already exists for this config and root
950 do i = 1, manager%num_servers
951 if (manager%servers(i)%config_index == config_index .and. &
952 manager%servers(i)%root_path == root_path) then
953 server_index = i
954 return
955 end if
956 end do
957
958 ! Start new server
959 server_index = start_new_server_from_config(manager, config_index, root_path)
960 end function get_or_start_server_by_config
961
962 ! Start a new server from a specific config
963 function start_new_server_from_config(manager, config_index, root_path) result(server_index)
964 type(lsp_manager_t), intent(inout) :: manager
965 integer, intent(in) :: config_index
966 character(len=*), intent(in) :: root_path
967 integer :: server_index
968 type(lsp_server_t), allocatable :: new_servers(:)
969 character(len=:), allocatable :: command
970
971 server_index = 0
972 if (config_index < 1 .or. config_index > manager%num_configs) return
973
974 command = manager%configs(config_index)%command
975
976 ! Expand server array
977 allocate(new_servers(manager%num_servers + 1))
978 if (manager%num_servers > 0) then
979 new_servers(1:manager%num_servers) = manager%servers
980 end if
981
982 ! Initialize new server
983 new_servers(manager%num_servers + 1)%language = manager%configs(config_index)%language
984 new_servers(manager%num_servers + 1)%name = manager%configs(config_index)%name
985 new_servers(manager%num_servers + 1)%command = command
986 new_servers(manager%num_servers + 1)%root_path = root_path
987 new_servers(manager%num_servers + 1)%config_index = config_index
988 new_servers(manager%num_servers + 1)%initialized = .false.
989 new_servers(manager%num_servers + 1)%initializing = .false.
990 allocate(new_servers(manager%num_servers + 1)%pending_requests(100))
991 new_servers(manager%num_servers + 1)%num_pending = 0
992 new_servers(manager%num_servers + 1)%read_buffer = ""
993
994 ! Start the server process
995 call lsp_start_server_f(command//c_null_char, len(command), &
996 new_servers(manager%num_servers + 1)%handle)
997
998 if (c_associated(new_servers(manager%num_servers + 1)%handle)) then
999 new_servers(manager%num_servers + 1)%process_id = &
1000 lsp_get_pid_f(new_servers(manager%num_servers + 1)%handle)
1001
1002 deallocate(manager%servers)
1003 manager%servers = new_servers
1004 manager%num_servers = manager%num_servers + 1
1005 server_index = manager%num_servers
1006
1007 ! Send initialization request
1008 call initialize_server(manager%servers(server_index))
1009 else
1010 write(error_unit, '(a,a)') "Failed to start LSP server: ", command
1011 end if
1012 end function start_new_server_from_config
1013
1014 ! Get the first server from a list of indices that has a specific capability
1015 function get_server_with_capability(manager, server_indices, num_servers, capability) result(server_index)
1016 type(lsp_manager_t), intent(in) :: manager
1017 integer, intent(in) :: server_indices(:)
1018 integer, intent(in) :: num_servers
1019 integer, intent(in) :: capability
1020 integer :: server_index
1021 integer :: i, cfg_idx
1022
1023 server_index = 0
1024
1025 do i = 1, num_servers
1026 if (server_indices(i) > 0 .and. server_indices(i) <= manager%num_servers) then
1027 cfg_idx = manager%servers(server_indices(i))%config_index
1028 if (cfg_idx > 0 .and. cfg_idx <= manager%num_configs) then
1029 if (manager%configs(cfg_idx)%capabilities(capability)) then
1030 server_index = server_indices(i)
1031 return
1032 end if
1033 end if
1034 end if
1035 end do
1036 end function get_server_with_capability
1037
1038 ! Send textDocument/didOpen notification
1039 subroutine notify_file_opened(manager, server_index, filename, content)
1040 use lsp_protocol_module, only: create_did_open_notification
1041 type(lsp_manager_t), intent(inout) :: manager
1042 integer, intent(in) :: server_index
1043 character(len=*), intent(in) :: filename
1044 character(len=*), intent(in) :: content
1045 type(lsp_message_t) :: msg
1046 character(len=:), allocatable :: language
1047 type(pending_didopen_t), allocatable :: temp_pending(:)
1048 integer :: i
1049
1050 if (server_index < 1 .or. server_index > manager%num_servers) return
1051
1052 ! If server not initialized yet, queue the notification
1053 if (.not. manager%servers(server_index)%initialized) then
1054 ! Add to pending queue
1055 if (allocated(manager%servers(server_index)%pending_didopens)) then
1056 ! Grow array
1057 allocate(temp_pending(manager%servers(server_index)%num_pending_didopens + 1))
1058 do i = 1, manager%servers(server_index)%num_pending_didopens
1059 temp_pending(i) = manager%servers(server_index)%pending_didopens(i)
1060 end do
1061 deallocate(manager%servers(server_index)%pending_didopens)
1062 allocate(manager%servers(server_index)%pending_didopens(size(temp_pending)))
1063 manager%servers(server_index)%pending_didopens = temp_pending
1064 deallocate(temp_pending)
1065 else
1066 allocate(manager%servers(server_index)%pending_didopens(1))
1067 end if
1068
1069 manager%servers(server_index)%num_pending_didopens = &
1070 manager%servers(server_index)%num_pending_didopens + 1
1071
1072 ! Store filename and content
1073 allocate(character(len=len(filename)) :: &
1074 manager%servers(server_index)%pending_didopens( &
1075 manager%servers(server_index)%num_pending_didopens)%filename)
1076 manager%servers(server_index)%pending_didopens( &
1077 manager%servers(server_index)%num_pending_didopens)%filename = filename
1078
1079 allocate(character(len=len(content)) :: &
1080 manager%servers(server_index)%pending_didopens( &
1081 manager%servers(server_index)%num_pending_didopens)%content)
1082 manager%servers(server_index)%pending_didopens( &
1083 manager%servers(server_index)%num_pending_didopens)%content = content
1084
1085 return
1086 end if
1087
1088 ! Server is ready, send immediately
1089 language = get_language_for_file(filename)
1090 block
1091 character(len=:), allocatable :: file_uri
1092 file_uri = filename_to_uri(filename)
1093 msg = create_did_open_notification(file_uri, language, 1, content)
1094 end block
1095
1096 call send_notification(manager%servers(server_index), msg)
1097 end subroutine notify_file_opened
1098
1099 ! Send textDocument/didChange notification
1100 subroutine notify_file_changed(manager, server_index, filename, content, version)
1101 use lsp_protocol_module, only: create_did_change_notification
1102 type(lsp_manager_t), intent(inout) :: manager
1103 integer, intent(in) :: server_index
1104 character(len=*), intent(in) :: filename
1105 character(len=*), intent(in) :: content
1106 integer, intent(in), optional :: version
1107 type(lsp_message_t) :: msg
1108 integer :: doc_version
1109 integer :: debug_unit
1110
1111 ! Debug logging
1112 open(newunit=debug_unit, file='/tmp/fac_didchange_debug.log', status='unknown', &
1113 position='append', action='write')
1114 write(debug_unit, '(A,I2,A)') 'notify_file_changed called for server ', server_index, ':'
1115 write(debug_unit, '(A,A)') ' URI: ', trim(filename)
1116 write(debug_unit, '(A,I8)') ' Content length: ', len(content)
1117 write(debug_unit, '(A)') '---'
1118 close(debug_unit)
1119
1120 if (server_index < 1 .or. server_index > manager%num_servers) return
1121 if (.not. manager%servers(server_index)%initialized) return
1122
1123 ! Use provided version or default to 1
1124 doc_version = 1
1125 if (present(version)) doc_version = version
1126
1127 msg = create_did_change_notification(filename, doc_version, content)
1128 call send_notification(manager%servers(server_index), msg)
1129 end subroutine notify_file_changed
1130
1131 ! Send textDocument/didSave notification
1132 subroutine notify_file_saved(manager, server_index, filename, content)
1133 use lsp_protocol_module, only: create_did_save_notification
1134 type(lsp_manager_t), intent(inout) :: manager
1135 integer, intent(in) :: server_index
1136 character(len=*), intent(in) :: filename
1137 character(len=*), intent(in), optional :: content
1138 type(lsp_message_t) :: msg
1139 character(len=:), allocatable :: file_uri
1140
1141 if (server_index < 1 .or. server_index > manager%num_servers) return
1142 if (.not. manager%servers(server_index)%initialized) return
1143
1144 ! Convert filename to URI
1145 file_uri = filename_to_uri(filename)
1146
1147 ! Create and send the notification
1148 if (present(content)) then
1149 msg = create_did_save_notification(file_uri, content)
1150 else
1151 msg = create_did_save_notification(file_uri)
1152 end if
1153
1154 call send_notification(manager%servers(server_index), msg)
1155 end subroutine notify_file_saved
1156
1157 ! Send textDocument/didClose notification
1158 subroutine notify_file_closed(manager, server_index, filename)
1159 use lsp_protocol_module, only: create_did_close_notification
1160 type(lsp_manager_t), intent(inout) :: manager
1161 integer, intent(in) :: server_index
1162 character(len=*), intent(in) :: filename
1163 type(lsp_message_t) :: msg
1164
1165 if (server_index < 1 .or. server_index > manager%num_servers) return
1166 if (.not. manager%servers(server_index)%initialized) return
1167
1168 msg = create_did_close_notification(filename)
1169 call send_notification(manager%servers(server_index), msg)
1170 end subroutine notify_file_closed
1171
1172 ! Request code completion at cursor position
1173 function request_completion(manager, server_index, filename, line, character, callback) result(request_id)
1174 use lsp_protocol_module, only: create_completion_request
1175 type(lsp_manager_t), intent(inout) :: manager
1176 integer, intent(in) :: server_index
1177 character(len=*), intent(in) :: filename
1178 integer, intent(in) :: line, character ! 0-based LSP positions
1179 procedure(response_callback), optional :: callback
1180 integer :: request_id
1181 type(lsp_message_t) :: msg
1182 character(len=:), allocatable :: uri
1183
1184 request_id = -1
1185 if (server_index < 1 .or. server_index > manager%num_servers) return
1186 if (.not. manager%servers(server_index)%initialized) return
1187
1188 ! Convert filename to URI (simple file:// for now)
1189 uri = filename_to_uri(filename)
1190
1191 msg = create_completion_request(trim(uri), line, character)
1192 request_id = msg%id
1193
1194 call send_request(manager, server_index, msg, callback)
1195 end function request_completion
1196
1197 ! Request hover information at cursor position
1198 function request_hover(manager, server_index, filename, line, character, callback) result(request_id)
1199 use lsp_protocol_module, only: create_hover_request
1200 type(lsp_manager_t), intent(inout) :: manager
1201 integer, intent(in) :: server_index
1202 character(len=*), intent(in) :: filename
1203 integer, intent(in) :: line, character ! 0-based LSP positions
1204 procedure(response_callback), optional :: callback
1205 integer :: request_id
1206 type(lsp_message_t) :: msg
1207 character(len=:), allocatable :: uri
1208
1209 request_id = -1
1210 if (server_index < 1 .or. server_index > manager%num_servers) return
1211 if (.not. manager%servers(server_index)%initialized) return
1212
1213 ! Convert filename to URI (simple file:// for now)
1214 uri = filename_to_uri(filename)
1215
1216 msg = create_hover_request(trim(uri), line, character)
1217 request_id = msg%id
1218
1219 call send_request(manager, server_index, msg, callback)
1220 end function request_hover
1221
1222 ! Request definition location at cursor position
1223 function request_definition(manager, server_index, filename, line, character, callback) result(request_id)
1224 use lsp_protocol_module, only: create_definition_request
1225 type(lsp_manager_t), intent(inout) :: manager
1226 integer, intent(in) :: server_index
1227 character(len=*), intent(in) :: filename
1228 integer, intent(in) :: line, character ! 0-based LSP positions
1229 procedure(response_callback), optional :: callback
1230 integer :: request_id
1231 type(lsp_message_t) :: msg
1232 character(len=:), allocatable :: uri
1233
1234 request_id = -1
1235 if (server_index < 1 .or. server_index > manager%num_servers) return
1236 if (.not. manager%servers(server_index)%initialized) return
1237
1238 uri = filename_to_uri(filename)
1239 msg = create_definition_request(uri, line, character)
1240 request_id = msg%id
1241 call send_request(manager, server_index, msg, callback)
1242 end function request_definition
1243
1244 ! Request references at cursor position
1245 function request_references(manager, server_index, filename, line, character, callback) result(request_id)
1246 use lsp_protocol_module, only: create_references_request
1247 type(lsp_manager_t), intent(inout) :: manager
1248 integer, intent(in) :: server_index
1249 character(len=*), intent(in) :: filename
1250 integer, intent(in) :: line, character ! 0-based LSP positions
1251 procedure(response_callback), optional :: callback
1252 integer :: request_id
1253 type(lsp_message_t) :: msg
1254 character(len=:), allocatable :: uri
1255
1256 request_id = -1
1257 if (server_index < 1 .or. server_index > manager%num_servers) return
1258 if (.not. manager%servers(server_index)%initialized) return
1259
1260 ! Convert filename to URI (simple file:// for now)
1261 uri = filename_to_uri(filename)
1262
1263 ! Include declaration and references
1264 msg = create_references_request(uri, line, character, .true.)
1265 request_id = msg%id
1266
1267 call send_request(manager, server_index, msg, callback)
1268 end function request_references
1269
1270 ! Request code actions for a range
1271 function request_code_actions(manager, server_index, filename, start_line, start_char, &
1272 end_line, end_char, callback, diagnostics_json) result(request_id)
1273 use lsp_protocol_module, only: create_code_action_request
1274 use json_module, only: json_create_array, json_value_t
1275 type(lsp_manager_t), intent(inout) :: manager
1276 integer, intent(in) :: server_index
1277 character(len=*), intent(in) :: filename
1278 integer, intent(in) :: start_line, start_char, end_line, end_char ! 0-based LSP positions
1279 procedure(response_callback), optional :: callback
1280 type(json_value_t), intent(in), optional :: diagnostics_json
1281 integer :: request_id
1282 type(lsp_message_t) :: msg
1283 character(len=:), allocatable :: uri
1284
1285 request_id = -1
1286
1287 if (server_index < 1 .or. server_index > manager%num_servers) return
1288 if (.not. manager%servers(server_index)%initialized) return
1289
1290 ! Convert filename to URI
1291 uri = filename_to_uri(filename)
1292
1293 ! Create code action request with diagnostics context
1294 if (present(diagnostics_json)) then
1295 msg = create_code_action_request(uri, start_line, start_char, end_line, end_char, diagnostics_json)
1296 else
1297 msg = create_code_action_request(uri, start_line, start_char, end_line, end_char)
1298 end if
1299 request_id = msg%id
1300
1301 call send_request(manager, server_index, msg, callback)
1302 end function request_code_actions
1303
1304 ! Request document symbols
1305 function request_document_symbols(manager, server_index, filename, callback) result(request_id)
1306 use lsp_protocol_module, only: create_document_symbols_request
1307 type(lsp_manager_t), intent(inout) :: manager
1308 integer, intent(in) :: server_index
1309 character(len=*), intent(in) :: filename
1310 procedure(response_callback), optional :: callback
1311 integer :: request_id
1312 type(lsp_message_t) :: msg
1313 character(len=:), allocatable :: uri
1314
1315 request_id = -1
1316 if (server_index < 1 .or. server_index > manager%num_servers) return
1317 if (.not. manager%servers(server_index)%initialized) return
1318
1319 ! Convert filename to URI
1320 uri = filename_to_uri(filename)
1321
1322 ! Create document symbols request
1323 msg = create_document_symbols_request(uri)
1324 request_id = msg%id
1325
1326 call send_request(manager, server_index, msg, callback)
1327 end function request_document_symbols
1328
1329 ! Request signature help
1330 function request_signature_help(manager, server_index, filename, line, character, callback) result(request_id)
1331 use lsp_protocol_module, only: create_signature_help_request
1332 type(lsp_manager_t), intent(inout) :: manager
1333 integer, intent(in) :: server_index
1334 character(len=*), intent(in) :: filename
1335 integer, intent(in) :: line, character ! 0-based LSP positions
1336 procedure(response_callback), optional :: callback
1337 integer :: request_id
1338 type(lsp_message_t) :: msg
1339 character(len=:), allocatable :: uri
1340
1341 request_id = -1
1342 if (server_index < 1 .or. server_index > manager%num_servers) return
1343 if (.not. manager%servers(server_index)%initialized) return
1344
1345 ! Convert filename to URI
1346 uri = filename_to_uri(filename)
1347
1348 ! Create signature help request
1349 msg = create_signature_help_request(uri, line, character)
1350 request_id = msg%id
1351
1352 call send_request(manager, server_index, msg, callback)
1353 end function request_signature_help
1354
1355 ! Request formatting
1356 function request_formatting(manager, server_index, filename, tab_size, insert_spaces, callback) result(request_id)
1357 use lsp_protocol_module, only: create_formatting_request
1358 type(lsp_manager_t), intent(inout) :: manager
1359 integer, intent(in) :: server_index
1360 character(len=*), intent(in) :: filename
1361 integer, intent(in) :: tab_size
1362 logical, intent(in) :: insert_spaces
1363 procedure(response_callback), optional :: callback
1364 integer :: request_id
1365 type(lsp_message_t) :: msg
1366 character(len=:), allocatable :: uri
1367
1368 request_id = -1
1369 if (server_index < 1 .or. server_index > manager%num_servers) return
1370 if (.not. manager%servers(server_index)%initialized) return
1371
1372 ! Convert filename to URI
1373 uri = filename_to_uri(filename)
1374
1375 ! Create formatting request
1376 msg = create_formatting_request(uri, tab_size, insert_spaces)
1377 request_id = msg%id
1378
1379 call send_request(manager, server_index, msg, callback)
1380 end function request_formatting
1381
1382 ! Request rename
1383 function request_rename(manager, server_index, filename, line, character, new_name, callback) result(request_id)
1384 use lsp_protocol_module, only: create_rename_request
1385 type(lsp_manager_t), intent(inout) :: manager
1386 integer, intent(in) :: server_index
1387 character(len=*), intent(in) :: filename
1388 integer, intent(in) :: line, character ! 0-based LSP positions
1389 character(len=*), intent(in) :: new_name
1390 procedure(response_callback), optional :: callback
1391 integer :: request_id
1392 type(lsp_message_t) :: msg
1393 character(len=:), allocatable :: uri
1394
1395 request_id = -1
1396 if (server_index < 1 .or. server_index > manager%num_servers) return
1397 if (.not. manager%servers(server_index)%initialized) return
1398
1399 ! Convert filename to URI
1400 uri = filename_to_uri(filename)
1401
1402 ! Create rename request
1403 msg = create_rename_request(uri, line, character, new_name)
1404 request_id = msg%id
1405
1406 call send_request(manager, server_index, msg, callback)
1407 end function request_rename
1408
1409 ! Request workspace symbols
1410 function request_workspace_symbols(manager, server_index, query, callback) result(request_id)
1411 use lsp_protocol_module, only: create_workspace_symbols_request
1412 type(lsp_manager_t), intent(inout) :: manager
1413 integer, intent(in) :: server_index
1414 character(len=*), intent(in) :: query
1415 procedure(response_callback), optional :: callback
1416 integer :: request_id
1417 type(lsp_message_t) :: msg
1418
1419 request_id = -1
1420 if (server_index < 1 .or. server_index > manager%num_servers) return
1421 if (.not. manager%servers(server_index)%initialized) return
1422 if (.not. manager%servers(server_index)%supports_workspace_symbols) return
1423
1424 ! Create workspace symbols request
1425 msg = create_workspace_symbols_request(query)
1426 request_id = msg%id
1427
1428 call send_request(manager, server_index, msg, callback)
1429 end function request_workspace_symbols
1430
1431 ! Set the diagnostics notification handler
1432 subroutine set_diagnostics_handler(manager, handler)
1433 type(lsp_manager_t), intent(inout) :: manager
1434 procedure(diagnostics_callback) :: handler
1435
1436 manager%diagnostics_handler => handler
1437 end subroutine set_diagnostics_handler
1438
1439 end module lsp_server_manager_module