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