Fortran · 41078 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 :: notify_file_opened, notify_file_changed, notify_file_saved, notify_file_closed
19 public :: request_completion, request_hover, request_definition, request_references, request_code_actions
20 public :: request_document_symbols, request_signature_help, request_formatting, request_rename
21 public :: request_workspace_symbols
22 public :: set_diagnostics_handler
23
24 ! Language server configuration
25 type :: server_config_t
26 character(len=:), allocatable :: language
27 character(len=:), allocatable :: command
28 character(len=:), allocatable :: file_patterns
29 end type server_config_t
30
31 ! Language server instance
32 type :: lsp_server_t
33 type(c_ptr) :: handle = c_null_ptr
34 character(len=:), allocatable :: language
35 character(len=:), allocatable :: command
36 character(len=:), allocatable :: root_path
37 logical :: initialized = .false.
38 logical :: initializing = .false.
39 integer :: process_id = -1
40
41 ! Capabilities
42 logical :: supports_completion = .false.
43 logical :: supports_hover = .false.
44 logical :: supports_definition = .false.
45 logical :: supports_references = .false.
46 logical :: supports_code_actions = .false.
47 logical :: supports_document_symbols = .false.
48 logical :: supports_workspace_symbols = .false.
49 logical :: supports_formatting = .false.
50 logical :: supports_rename = .false.
51
52 ! Request tracking
53 integer, allocatable :: pending_requests(:)
54 integer :: num_pending = 0
55
56 ! Message buffer
57 character(len=:), allocatable :: read_buffer
58 end type lsp_server_t
59
60 ! Callback type for responses
61 abstract interface
62 subroutine response_callback(request_id, response)
63 use lsp_protocol_module, only: lsp_message_t
64 integer, intent(in) :: request_id
65 type(lsp_message_t), intent(in) :: response
66 end subroutine response_callback
67 end interface
68
69 ! Callback type for diagnostics notifications
70 abstract interface
71 subroutine diagnostics_callback(notification)
72 use lsp_protocol_module, only: lsp_message_t
73 type(lsp_message_t), intent(in) :: notification
74 end subroutine diagnostics_callback
75 end interface
76
77 ! Request callback entry
78 type :: callback_entry_t
79 integer :: request_id
80 procedure(response_callback), pointer, nopass :: callback => null()
81 end type callback_entry_t
82
83 ! Main LSP manager
84 type :: lsp_manager_t
85 type(lsp_server_t), allocatable :: servers(:)
86 integer :: num_servers = 0
87 type(server_config_t), allocatable :: configs(:)
88 integer :: num_configs = 0
89 type(callback_entry_t), allocatable :: callbacks(:)
90 integer :: num_callbacks = 0
91 procedure(diagnostics_callback), pointer, nopass :: diagnostics_handler => null()
92 end type lsp_manager_t
93
94 ! C interfaces
95 interface
96 subroutine lsp_start_server_f(command, command_len, handle) bind(c, name='lsp_start_server_f')
97 use iso_c_binding
98 character(c_char), intent(in) :: command(*)
99 integer(c_int), value :: command_len
100 type(c_ptr), intent(out) :: handle
101 end subroutine lsp_start_server_f
102
103 subroutine lsp_stop_server_f(handle) bind(c, name='lsp_stop_server_f')
104 use iso_c_binding
105 type(c_ptr), intent(inout) :: handle
106 end subroutine lsp_stop_server_f
107
108 function lsp_send_message_f(handle, message, message_len) bind(c, name='lsp_send_message_f')
109 use iso_c_binding
110 integer(c_int) :: lsp_send_message_f
111 type(c_ptr), intent(in) :: handle
112 character(c_char), intent(in) :: message(*)
113 integer(c_int), value :: message_len
114 end function lsp_send_message_f
115
116 function lsp_read_message_f(handle, buffer, buffer_len) bind(c, name='lsp_read_message_f')
117 use iso_c_binding
118 integer(c_int) :: lsp_read_message_f
119 type(c_ptr), intent(in) :: handle
120 character(c_char), intent(out) :: buffer(*)
121 integer(c_int), value :: buffer_len
122 end function lsp_read_message_f
123
124 function lsp_is_running_f(handle) bind(c, name='lsp_is_running_f')
125 use iso_c_binding
126 integer(c_int) :: lsp_is_running_f
127 type(c_ptr), intent(in) :: handle
128 end function lsp_is_running_f
129
130 function lsp_get_pid_f(handle) bind(c, name='lsp_get_pid_f')
131 use iso_c_binding
132 integer(c_int) :: lsp_get_pid_f
133 type(c_ptr), intent(in) :: handle
134 end function lsp_get_pid_f
135 end interface
136
137 contains
138
139 subroutine init_lsp_manager(manager)
140 type(lsp_manager_t), intent(out) :: manager
141
142 allocate(manager%servers(0))
143 allocate(manager%configs(0))
144 allocate(manager%callbacks(0))
145
146 ! Load default server configurations
147 call load_default_configs(manager)
148 end subroutine init_lsp_manager
149
150 subroutine cleanup_lsp_manager(manager)
151 type(lsp_manager_t), intent(inout) :: manager
152 integer :: i
153
154 ! Stop all servers
155 do i = 1, manager%num_servers
156 call stop_server(manager%servers(i))
157 end do
158
159 if (allocated(manager%servers)) deallocate(manager%servers)
160 if (allocated(manager%configs)) deallocate(manager%configs)
161 if (allocated(manager%callbacks)) deallocate(manager%callbacks)
162
163 manager%num_servers = 0
164 manager%num_configs = 0
165 manager%num_callbacks = 0
166 end subroutine cleanup_lsp_manager
167
168 subroutine load_default_configs(manager)
169 type(lsp_manager_t), intent(inout) :: manager
170
171 ! Python
172 call add_config(manager, "python", "pylsp", "*.py")
173
174 ! Rust
175 call add_config(manager, "rust", "rust-analyzer", "*.rs")
176
177 ! C/C++
178 call add_config(manager, "c", "clangd", "*.c,*.h")
179 call add_config(manager, "cpp", "clangd", "*.cpp,*.cc,*.cxx,*.hpp,*.hxx")
180
181 ! Go
182 call add_config(manager, "go", "gopls", "*.go")
183
184 ! TypeScript/JavaScript
185 call add_config(manager, "typescript", "typescript-language-server --stdio", "*.ts,*.tsx")
186 call add_config(manager, "javascript", "typescript-language-server --stdio", "*.js,*.jsx")
187
188 ! Fortran
189 call add_config(manager, "fortran", "fortls", "*.f90,*.f95,*.f03,*.f08")
190
191 ! TODO: Load from config file
192 end subroutine load_default_configs
193
194 subroutine add_config(manager, language, command, patterns)
195 type(lsp_manager_t), intent(inout) :: manager
196 character(len=*), intent(in) :: language, command, patterns
197 type(server_config_t), allocatable :: new_configs(:)
198
199 allocate(new_configs(manager%num_configs + 1))
200 if (manager%num_configs > 0) then
201 new_configs(1:manager%num_configs) = manager%configs
202 end if
203
204 new_configs(manager%num_configs + 1)%language = language
205 new_configs(manager%num_configs + 1)%command = command
206 new_configs(manager%num_configs + 1)%file_patterns = patterns
207
208 deallocate(manager%configs)
209 manager%configs = new_configs
210 manager%num_configs = manager%num_configs + 1
211 end subroutine add_config
212
213 function get_or_start_server(manager, language, root_path) result(server_index)
214 type(lsp_manager_t), intent(inout) :: manager
215 character(len=*), intent(in) :: language, root_path
216 integer :: server_index
217 integer :: i
218
219 ! Check if server already exists
220 do i = 1, manager%num_servers
221 if (manager%servers(i)%language == language .and. &
222 manager%servers(i)%root_path == root_path) then
223 server_index = i
224 return
225 end if
226 end do
227
228 ! Start new server
229 server_index = start_new_server(manager, language, root_path)
230 end function get_or_start_server
231
232 function start_new_server(manager, language, root_path) result(server_index)
233 type(lsp_manager_t), intent(inout) :: manager
234 character(len=*), intent(in) :: language, root_path
235 integer :: server_index
236 type(lsp_server_t), allocatable :: new_servers(:)
237 character(len=:), allocatable :: command
238 integer :: i
239
240 server_index = 0
241
242 ! Find command for language
243 command = ""
244 do i = 1, manager%num_configs
245 if (manager%configs(i)%language == language) then
246 command = manager%configs(i)%command
247 exit
248 end if
249 end do
250
251 if (command == "") then
252 write(error_unit, '(a,a)') "No LSP server configured for language: ", language
253 return
254 end if
255
256 ! Expand server array
257 allocate(new_servers(manager%num_servers + 1))
258 if (manager%num_servers > 0) then
259 new_servers(1:manager%num_servers) = manager%servers
260 end if
261
262 ! Initialize new server
263 new_servers(manager%num_servers + 1)%language = language
264 new_servers(manager%num_servers + 1)%command = command
265 new_servers(manager%num_servers + 1)%root_path = root_path
266 new_servers(manager%num_servers + 1)%initialized = .false.
267 new_servers(manager%num_servers + 1)%initializing = .false.
268 allocate(new_servers(manager%num_servers + 1)%pending_requests(100))
269 new_servers(manager%num_servers + 1)%num_pending = 0
270 new_servers(manager%num_servers + 1)%read_buffer = ""
271
272 ! Start the server process
273 call lsp_start_server_f(command//c_null_char, len(command), &
274 new_servers(manager%num_servers + 1)%handle)
275
276 if (c_associated(new_servers(manager%num_servers + 1)%handle)) then
277 new_servers(manager%num_servers + 1)%process_id = &
278 lsp_get_pid_f(new_servers(manager%num_servers + 1)%handle)
279
280 deallocate(manager%servers)
281 manager%servers = new_servers
282 manager%num_servers = manager%num_servers + 1
283 server_index = manager%num_servers
284
285 ! Send initialization request
286 call initialize_server(manager%servers(server_index))
287 else
288 write(error_unit, '(a,a)') "Failed to start LSP server: ", command
289 end if
290 end function start_new_server
291
292 subroutine initialize_server(server)
293 type(lsp_server_t), intent(inout) :: server
294 type(lsp_message_t) :: msg
295 character(len=:), allocatable :: json_msg
296
297 if (server%initializing .or. server%initialized) return
298
299 server%initializing = .true.
300
301 ! Create initialization request
302 msg = create_initialize_request(server%process_id, server%root_path, "fac")
303
304 ! Send it
305 json_msg = format_json_rpc(msg)
306 call send_raw_message(server, json_msg)
307
308 ! Track the request
309 call track_request(server, msg%id)
310 end subroutine initialize_server
311
312 subroutine stop_server(server)
313 type(lsp_server_t), intent(inout) :: server
314
315 if (c_associated(server%handle)) then
316 ! Send shutdown request
317 ! TODO: Implement proper shutdown sequence
318
319 call lsp_stop_server_f(server%handle)
320 server%handle = c_null_ptr
321 end if
322
323 server%initialized = .false.
324 server%initializing = .false.
325 server%process_id = -1
326 end subroutine stop_server
327
328 subroutine send_request(manager, server_index, msg, callback)
329 type(lsp_manager_t), intent(inout) :: manager
330 integer, intent(in) :: server_index
331 type(lsp_message_t), intent(in) :: msg
332 procedure(response_callback), optional :: callback
333 character(len=:), allocatable :: json_msg
334
335 if (server_index < 1 .or. server_index > manager%num_servers) return
336 if (.not. manager%servers(server_index)%initialized .and. &
337 .not. manager%servers(server_index)%initializing) return
338
339 json_msg = format_json_rpc(msg)
340 call send_raw_message(manager%servers(server_index), json_msg)
341
342 ! Track request and register callback
343 call track_request(manager%servers(server_index), msg%id)
344 if (present(callback)) then
345 call register_callback(manager, msg%id, callback)
346 end if
347 end subroutine send_request
348
349 subroutine send_notification(server, msg)
350 type(lsp_server_t), intent(inout) :: server
351 type(lsp_message_t), intent(in) :: msg
352 character(len=:), allocatable :: json_msg
353
354 if (.not. server%initialized .and. .not. server%initializing) return
355
356 json_msg = format_json_rpc(msg)
357 call send_raw_message(server, json_msg)
358 end subroutine send_notification
359
360 subroutine send_raw_message(server, message)
361 type(lsp_server_t), intent(inout) :: server
362 character(len=*), intent(in) :: message
363 integer :: result
364
365 if (.not. c_associated(server%handle)) return
366
367 result = lsp_send_message_f(server%handle, message//c_null_char, len(message))
368
369 if (result < 0) then
370 write(error_unit, '(a)') "Failed to send message to LSP server"
371 end if
372 end subroutine send_raw_message
373
374 subroutine process_server_messages(manager)
375 type(lsp_manager_t), intent(inout) :: manager
376 integer :: i
377
378 do i = 1, manager%num_servers
379 if (c_associated(manager%servers(i)%handle)) then
380 call process_server_output(manager, manager%servers(i))
381 end if
382 end do
383 end subroutine process_server_messages
384
385 subroutine process_server_output(manager, server)
386 type(lsp_manager_t), intent(inout) :: manager
387 type(lsp_server_t), intent(inout) :: server
388 character(len=65536) :: buffer
389 integer :: bytes_read
390 type(lsp_message_t) :: msg
391
392 ! Read from server
393 bytes_read = lsp_read_message_f(server%handle, buffer, len(buffer))
394
395 if (bytes_read > 0) then
396 ! Append to buffer
397 server%read_buffer = server%read_buffer // buffer(1:bytes_read)
398
399 ! Try to parse complete messages
400 call parse_messages(manager, server)
401 end if
402 end subroutine process_server_output
403
404 subroutine parse_messages(manager, server)
405 type(lsp_manager_t), intent(inout) :: manager
406 type(lsp_server_t), intent(inout) :: server
407 integer :: content_length, header_end, message_end
408 character(len=:), allocatable :: message
409 type(lsp_message_t) :: msg
410
411 do
412 ! Look for Content-Length header
413 content_length = extract_content_length(server%read_buffer)
414 if (content_length <= 0) exit
415
416 ! Find where content starts
417 header_end = index(server%read_buffer, char(13)//char(10)//char(13)//char(10))
418 if (header_end <= 0) exit
419
420 ! Check if we have the complete message
421 message_end = header_end + 3 + content_length
422 if (len(server%read_buffer) < message_end) exit
423
424 ! Extract and parse message
425 message = server%read_buffer(1:message_end)
426 msg = parse_lsp_message(message)
427
428 ! Handle the message
429 call handle_message(manager, server, msg)
430
431 ! Remove from buffer
432 server%read_buffer = server%read_buffer(message_end+1:)
433 end do
434 end subroutine parse_messages
435
436 function extract_content_length(buffer) result(length)
437 character(len=*), intent(in) :: buffer
438 integer :: length
439 integer :: pos, end_pos
440 character(len=32) :: len_str
441
442 length = -1
443
444 pos = index(buffer, "Content-Length:")
445 if (pos <= 0) return
446
447 pos = pos + 15 ! Skip "Content-Length:"
448
449 ! Find end of line
450 end_pos = index(buffer(pos:), char(13))
451 if (end_pos <= 0) return
452
453 len_str = adjustl(buffer(pos:pos+end_pos-2))
454 read(len_str, '(i10)', iostat=pos) length
455 if (pos /= 0) length = -1
456 end function extract_content_length
457
458 subroutine handle_message(manager, server, msg)
459 type(lsp_manager_t), intent(inout) :: manager
460 type(lsp_server_t), intent(inout) :: server
461 type(lsp_message_t), intent(in) :: msg
462
463 if (msg%is_response) then
464 call handle_response(manager, server, msg)
465 else if (msg%is_notification) then
466 call handle_notification(manager, server, msg)
467 else if (msg%is_request) then
468 call handle_request(manager, server, msg)
469 end if
470 end subroutine handle_message
471
472 subroutine handle_response(manager, server, msg)
473 type(lsp_manager_t), intent(inout) :: manager
474 type(lsp_server_t), intent(inout) :: server
475 type(lsp_message_t), intent(in) :: msg
476 integer :: i
477
478 ! Remove from pending requests
479 call untrack_request(server, msg%id)
480
481 ! Special handling for initialization response
482 if (server%initializing .and. msg%id == 1) then
483 call handle_initialize_response(server, msg)
484 return
485 end if
486
487 ! Find and call registered callback
488 do i = 1, manager%num_callbacks
489 if (manager%callbacks(i)%request_id == msg%id) then
490 if (associated(manager%callbacks(i)%callback)) then
491 call manager%callbacks(i)%callback(msg%id, msg)
492 end if
493 ! Remove callback
494 call remove_callback(manager, i)
495 exit
496 end if
497 end do
498 end subroutine handle_response
499
500 subroutine handle_initialize_response(server, msg)
501 type(lsp_server_t), intent(inout) :: server
502 type(lsp_message_t), intent(in) :: msg
503 type(json_value_t) :: capabilities
504 type(lsp_message_t) :: initialized_msg
505
506 ! Parse server capabilities
507 capabilities = json_get_object(msg%result, "capabilities")
508
509 ! Check what the server supports
510 if (json_has_key(capabilities, "completionProvider")) then
511 server%supports_completion = .true.
512 end if
513
514 if (json_has_key(capabilities, "hoverProvider")) then
515 server%supports_hover = .true.
516 end if
517
518 if (json_has_key(capabilities, "definitionProvider")) then
519 server%supports_definition = .true.
520 end if
521
522 if (json_has_key(capabilities, "referencesProvider")) then
523 server%supports_references = .true.
524 end if
525
526 if (json_has_key(capabilities, "codeActionProvider")) then
527 server%supports_code_actions = .true.
528 end if
529
530 if (json_has_key(capabilities, "documentSymbolProvider")) then
531 server%supports_document_symbols = .true.
532 end if
533
534 if (json_has_key(capabilities, "workspaceSymbolProvider")) then
535 server%supports_workspace_symbols = .true.
536 end if
537
538 if (json_has_key(capabilities, "documentFormattingProvider")) then
539 server%supports_formatting = .true.
540 end if
541
542 if (json_has_key(capabilities, "renameProvider")) then
543 server%supports_rename = .true.
544 end if
545
546 if (json_has_key(capabilities, "codeActionProvider")) then
547 server%supports_code_actions = .true.
548 end if
549
550 ! Send initialized notification
551 initialized_msg = create_initialized_notification()
552 call send_notification(server, initialized_msg)
553
554 server%initialized = .true.
555 server%initializing = .false.
556
557 write(error_unit, '(a,a)') "LSP server initialized: ", server%language
558 end subroutine handle_initialize_response
559
560 subroutine handle_notification(manager, server, msg)
561 type(lsp_manager_t), intent(inout) :: manager
562 type(lsp_server_t), intent(inout) :: server
563 type(lsp_message_t), intent(in) :: msg
564
565 ! Debug: log all notifications
566 write(error_unit, '(A,A)') "[LSP DEBUG] Received notification: ", msg%method
567
568 select case(msg%method)
569 case("textDocument/publishDiagnostics")
570 write(error_unit, '(A)') "[LSP DEBUG] Processing publishDiagnostics"
571 ! Forward to diagnostics handler if set
572 if (associated(manager%diagnostics_handler)) then
573 call manager%diagnostics_handler(msg)
574 else
575 write(error_unit, '(A)') "[LSP DEBUG] No diagnostics handler set!"
576 end if
577 case("window/showMessage")
578 ! TODO: Show message to user
579 case("window/logMessage")
580 ! TODO: Log message
581 case default
582 ! Unknown notification
583 end select
584 end subroutine handle_notification
585
586 subroutine handle_request(manager, server, msg)
587 type(lsp_manager_t), intent(inout) :: manager
588 type(lsp_server_t), intent(inout) :: server
589 type(lsp_message_t), intent(in) :: msg
590
591 ! Servers can send requests too (like workspace/configuration)
592 ! TODO: Handle server requests
593 end subroutine handle_request
594
595 subroutine track_request(server, request_id)
596 type(lsp_server_t), intent(inout) :: server
597 integer, intent(in) :: request_id
598
599 if (server%num_pending < size(server%pending_requests)) then
600 server%num_pending = server%num_pending + 1
601 server%pending_requests(server%num_pending) = request_id
602 end if
603 end subroutine track_request
604
605 subroutine untrack_request(server, request_id)
606 type(lsp_server_t), intent(inout) :: server
607 integer, intent(in) :: request_id
608 integer :: i, j
609
610 do i = 1, server%num_pending
611 if (server%pending_requests(i) == request_id) then
612 ! Shift remaining requests
613 do j = i, server%num_pending - 1
614 server%pending_requests(j) = server%pending_requests(j + 1)
615 end do
616 server%num_pending = server%num_pending - 1
617 exit
618 end if
619 end do
620 end subroutine untrack_request
621
622 subroutine register_callback(manager, request_id, callback)
623 type(lsp_manager_t), intent(inout) :: manager
624 integer, intent(in) :: request_id
625 procedure(response_callback) :: callback
626 type(callback_entry_t), allocatable :: new_callbacks(:)
627
628 allocate(new_callbacks(manager%num_callbacks + 1))
629 if (manager%num_callbacks > 0) then
630 new_callbacks(1:manager%num_callbacks) = manager%callbacks
631 end if
632
633 new_callbacks(manager%num_callbacks + 1)%request_id = request_id
634 new_callbacks(manager%num_callbacks + 1)%callback => callback
635
636 deallocate(manager%callbacks)
637 manager%callbacks = new_callbacks
638 manager%num_callbacks = manager%num_callbacks + 1
639 end subroutine register_callback
640
641 subroutine remove_callback(manager, index)
642 type(lsp_manager_t), intent(inout) :: manager
643 integer, intent(in) :: index
644 integer :: i
645
646 if (index < 1 .or. index > manager%num_callbacks) return
647
648 ! Shift remaining callbacks
649 do i = index, manager%num_callbacks - 1
650 manager%callbacks(i) = manager%callbacks(i + 1)
651 end do
652
653 manager%num_callbacks = manager%num_callbacks - 1
654 end subroutine remove_callback
655
656 ! Helper to get language from filename
657 function get_language_for_file(filename) result(language)
658 character(len=*), intent(in) :: filename
659 character(len=:), allocatable :: language
660 integer :: dot_pos
661
662 ! Find last dot in filename
663 dot_pos = index(filename, '.', back=.true.)
664 if (dot_pos == 0) then
665 language = ""
666 return
667 end if
668
669 ! Match extension to language
670 select case(filename(dot_pos:))
671 case('.py')
672 language = "python"
673 case('.rs')
674 language = "rust"
675 case('.c', '.h')
676 language = "c"
677 case('.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.C', '.H')
678 language = "cpp"
679 case('.go')
680 language = "go"
681 case('.ts', '.tsx')
682 language = "typescript"
683 case('.js', '.jsx')
684 language = "javascript"
685 case('.f90', '.f95', '.f03', '.f08', '.F90', '.F95', '.F03', '.F08')
686 language = "fortran"
687 case('.java')
688 language = "java"
689 case('.rb')
690 language = "ruby"
691 case('.lua')
692 language = "lua"
693 case default
694 language = ""
695 end select
696 end function get_language_for_file
697
698 ! Start LSP server for a file if needed
699 function start_lsp_for_file(manager, filename) result(server_index)
700 type(lsp_manager_t), intent(inout) :: manager
701 character(len=*), intent(in) :: filename
702 integer :: server_index
703 character(len=:), allocatable :: language
704 character(len=256) :: workspace_path
705 integer :: slash_pos
706
707 server_index = 0
708
709 ! Get language from file extension
710 language = get_language_for_file(filename)
711 if (language == "") return
712
713 ! Extract workspace path from filename (directory containing file)
714 slash_pos = index(filename, '/', back=.true.)
715 if (slash_pos > 0) then
716 workspace_path = filename(1:slash_pos-1)
717 else
718 workspace_path = "."
719 end if
720
721 ! Get or start server for this language
722 server_index = get_or_start_server(manager, language, trim(workspace_path))
723 end function start_lsp_for_file
724
725 ! Send textDocument/didOpen notification
726 subroutine notify_file_opened(manager, server_index, filename, content)
727 use lsp_protocol_module, only: create_did_open_notification
728 type(lsp_manager_t), intent(inout) :: manager
729 integer, intent(in) :: server_index
730 character(len=*), intent(in) :: filename
731 character(len=*), intent(in) :: content
732 type(lsp_message_t) :: msg
733 character(len=:), allocatable :: language
734
735 if (server_index < 1 .or. server_index > manager%num_servers) return
736 if (.not. manager%servers(server_index)%initialized) return
737
738 language = get_language_for_file(filename)
739 msg = create_did_open_notification(filename, language, 1, content)
740 call send_notification(manager%servers(server_index), msg)
741 end subroutine notify_file_opened
742
743 ! Send textDocument/didChange notification
744 subroutine notify_file_changed(manager, server_index, filename, content, version)
745 use lsp_protocol_module, only: create_did_change_notification
746 type(lsp_manager_t), intent(inout) :: manager
747 integer, intent(in) :: server_index
748 character(len=*), intent(in) :: filename
749 character(len=*), intent(in) :: content
750 integer, intent(in), optional :: version
751 type(lsp_message_t) :: msg
752 integer :: doc_version
753
754 if (server_index < 1 .or. server_index > manager%num_servers) return
755 if (.not. manager%servers(server_index)%initialized) return
756
757 ! Use provided version or default to 1
758 doc_version = 1
759 if (present(version)) doc_version = version
760
761 msg = create_did_change_notification(filename, doc_version, content)
762 call send_notification(manager%servers(server_index), msg)
763 end subroutine notify_file_changed
764
765 ! Send textDocument/didSave notification
766 subroutine notify_file_saved(manager, server_index, filename, content)
767 use lsp_protocol_module, only: create_did_save_notification
768 type(lsp_manager_t), intent(inout) :: manager
769 integer, intent(in) :: server_index
770 character(len=*), intent(in) :: filename
771 character(len=*), intent(in), optional :: content
772 type(lsp_message_t) :: msg
773 character(len=:), allocatable :: file_uri
774
775 if (server_index < 1 .or. server_index > manager%num_servers) return
776 if (.not. manager%servers(server_index)%initialized) return
777
778 ! Convert filename to URI
779 file_uri = 'file://' // trim(filename)
780
781 ! Create and send the notification
782 if (present(content)) then
783 msg = create_did_save_notification(file_uri, content)
784 else
785 msg = create_did_save_notification(file_uri)
786 end if
787
788 call send_notification(manager%servers(server_index), msg)
789
790 ! Debug output
791 write(error_unit, '(A,A)') "[LSP DEBUG] Sent didSave for: ", trim(filename)
792 end subroutine notify_file_saved
793
794 ! Send textDocument/didClose notification
795 subroutine notify_file_closed(manager, server_index, filename)
796 use lsp_protocol_module, only: create_did_close_notification
797 type(lsp_manager_t), intent(inout) :: manager
798 integer, intent(in) :: server_index
799 character(len=*), intent(in) :: filename
800 type(lsp_message_t) :: msg
801
802 if (server_index < 1 .or. server_index > manager%num_servers) return
803 if (.not. manager%servers(server_index)%initialized) return
804
805 msg = create_did_close_notification(filename)
806 call send_notification(manager%servers(server_index), msg)
807 end subroutine notify_file_closed
808
809 ! Request code completion at cursor position
810 function request_completion(manager, server_index, filename, line, character, callback) result(request_id)
811 use lsp_protocol_module, only: create_completion_request
812 type(lsp_manager_t), intent(inout) :: manager
813 integer, intent(in) :: server_index
814 character(len=*), intent(in) :: filename
815 integer, intent(in) :: line, character ! 0-based LSP positions
816 procedure(response_callback), optional :: callback
817 integer :: request_id
818 type(lsp_message_t) :: msg
819 character(len=256) :: uri
820
821 request_id = -1
822 if (server_index < 1 .or. server_index > manager%num_servers) return
823 if (.not. manager%servers(server_index)%initialized) return
824
825 ! Convert filename to URI (simple file:// for now)
826 uri = "file://" // trim(filename)
827
828 msg = create_completion_request(trim(uri), line, character)
829 request_id = msg%id
830
831 call send_request(manager, server_index, msg, callback)
832 end function request_completion
833
834 ! Request hover information at cursor position
835 function request_hover(manager, server_index, filename, line, character, callback) result(request_id)
836 use lsp_protocol_module, only: create_hover_request
837 type(lsp_manager_t), intent(inout) :: manager
838 integer, intent(in) :: server_index
839 character(len=*), intent(in) :: filename
840 integer, intent(in) :: line, character ! 0-based LSP positions
841 procedure(response_callback), optional :: callback
842 integer :: request_id
843 type(lsp_message_t) :: msg
844 character(len=256) :: uri
845
846 request_id = -1
847 if (server_index < 1 .or. server_index > manager%num_servers) return
848 if (.not. manager%servers(server_index)%initialized) return
849
850 ! Convert filename to URI (simple file:// for now)
851 uri = "file://" // trim(filename)
852
853 msg = create_hover_request(trim(uri), line, character)
854 request_id = msg%id
855
856 call send_request(manager, server_index, msg, callback)
857 end function request_hover
858
859 ! Request definition location at cursor position
860 function request_definition(manager, server_index, filename, line, character, callback) result(request_id)
861 use lsp_protocol_module, only: create_definition_request
862 type(lsp_manager_t), intent(inout) :: manager
863 integer, intent(in) :: server_index
864 character(len=*), intent(in) :: filename
865 integer, intent(in) :: line, character ! 0-based LSP positions
866 procedure(response_callback), optional :: callback
867 integer :: request_id
868 type(lsp_message_t) :: msg
869 character(len=256) :: uri
870
871 request_id = -1
872 if (server_index < 1 .or. server_index > manager%num_servers) return
873 if (.not. manager%servers(server_index)%initialized) return
874
875 ! Convert filename to URI (simple file:// for now)
876 uri = "file://" // trim(filename)
877
878 msg = create_definition_request(uri, line, character)
879 request_id = msg%id
880
881 call send_request(manager, server_index, msg, callback)
882 end function request_definition
883
884 ! Request references at cursor position
885 function request_references(manager, server_index, filename, line, character, callback) result(request_id)
886 use lsp_protocol_module, only: create_references_request
887 type(lsp_manager_t), intent(inout) :: manager
888 integer, intent(in) :: server_index
889 character(len=*), intent(in) :: filename
890 integer, intent(in) :: line, character ! 0-based LSP positions
891 procedure(response_callback), optional :: callback
892 integer :: request_id
893 type(lsp_message_t) :: msg
894 character(len=256) :: uri
895
896 request_id = -1
897 if (server_index < 1 .or. server_index > manager%num_servers) return
898 if (.not. manager%servers(server_index)%initialized) return
899
900 ! Convert filename to URI (simple file:// for now)
901 uri = "file://" // trim(filename)
902
903 ! Include declaration and references
904 msg = create_references_request(uri, line, character, .true.)
905 request_id = msg%id
906
907 call send_request(manager, server_index, msg, callback)
908 end function request_references
909
910 ! Request code actions for a range
911 function request_code_actions(manager, server_index, filename, start_line, start_char, &
912 end_line, end_char, callback) result(request_id)
913 use lsp_protocol_module, only: create_code_action_request
914 use json_module, only: json_create_array
915 type(lsp_manager_t), intent(inout) :: manager
916 integer, intent(in) :: server_index
917 character(len=*), intent(in) :: filename
918 integer, intent(in) :: start_line, start_char, end_line, end_char ! 0-based LSP positions
919 procedure(response_callback), optional :: callback
920 integer :: request_id
921 type(lsp_message_t) :: msg
922 character(len=256) :: uri
923
924 request_id = -1
925 if (server_index < 1 .or. server_index > manager%num_servers) return
926 if (.not. manager%servers(server_index)%initialized) return
927
928 ! Convert filename to URI
929 uri = "file://" // trim(filename)
930
931 ! Create code action request (diagnostics will be added separately if needed)
932 msg = create_code_action_request(uri, start_line, start_char, end_line, end_char)
933 request_id = msg%id
934
935 call send_request(manager, server_index, msg, callback)
936 end function request_code_actions
937
938 ! Request document symbols
939 function request_document_symbols(manager, server_index, filename, callback) result(request_id)
940 use lsp_protocol_module, only: create_document_symbols_request
941 type(lsp_manager_t), intent(inout) :: manager
942 integer, intent(in) :: server_index
943 character(len=*), intent(in) :: filename
944 procedure(response_callback), optional :: callback
945 integer :: request_id
946 type(lsp_message_t) :: msg
947 character(len=256) :: uri
948
949 request_id = -1
950 if (server_index < 1 .or. server_index > manager%num_servers) return
951 if (.not. manager%servers(server_index)%initialized) return
952
953 ! Convert filename to URI
954 uri = "file://" // trim(filename)
955
956 ! Create document symbols request
957 msg = create_document_symbols_request(uri)
958 request_id = msg%id
959
960 call send_request(manager, server_index, msg, callback)
961 end function request_document_symbols
962
963 ! Request signature help
964 function request_signature_help(manager, server_index, filename, line, character, callback) result(request_id)
965 use lsp_protocol_module, only: create_signature_help_request
966 type(lsp_manager_t), intent(inout) :: manager
967 integer, intent(in) :: server_index
968 character(len=*), intent(in) :: filename
969 integer, intent(in) :: line, character ! 0-based LSP positions
970 procedure(response_callback), optional :: callback
971 integer :: request_id
972 type(lsp_message_t) :: msg
973 character(len=256) :: uri
974
975 request_id = -1
976 if (server_index < 1 .or. server_index > manager%num_servers) return
977 if (.not. manager%servers(server_index)%initialized) return
978
979 ! Convert filename to URI
980 uri = "file://" // trim(filename)
981
982 ! Create signature help request
983 msg = create_signature_help_request(uri, line, character)
984 request_id = msg%id
985
986 call send_request(manager, server_index, msg, callback)
987 end function request_signature_help
988
989 ! Request formatting
990 function request_formatting(manager, server_index, filename, tab_size, insert_spaces, callback) result(request_id)
991 use lsp_protocol_module, only: create_formatting_request
992 type(lsp_manager_t), intent(inout) :: manager
993 integer, intent(in) :: server_index
994 character(len=*), intent(in) :: filename
995 integer, intent(in) :: tab_size
996 logical, intent(in) :: insert_spaces
997 procedure(response_callback), optional :: callback
998 integer :: request_id
999 type(lsp_message_t) :: msg
1000 character(len=256) :: uri
1001
1002 request_id = -1
1003 if (server_index < 1 .or. server_index > manager%num_servers) return
1004 if (.not. manager%servers(server_index)%initialized) return
1005
1006 ! Convert filename to URI
1007 uri = "file://" // trim(filename)
1008
1009 ! Create formatting request
1010 msg = create_formatting_request(uri, tab_size, insert_spaces)
1011 request_id = msg%id
1012
1013 call send_request(manager, server_index, msg, callback)
1014 end function request_formatting
1015
1016 ! Request rename
1017 function request_rename(manager, server_index, filename, line, character, new_name, callback) result(request_id)
1018 use lsp_protocol_module, only: create_rename_request
1019 type(lsp_manager_t), intent(inout) :: manager
1020 integer, intent(in) :: server_index
1021 character(len=*), intent(in) :: filename
1022 integer, intent(in) :: line, character ! 0-based LSP positions
1023 character(len=*), intent(in) :: new_name
1024 procedure(response_callback), optional :: callback
1025 integer :: request_id
1026 type(lsp_message_t) :: msg
1027 character(len=256) :: uri
1028
1029 request_id = -1
1030 if (server_index < 1 .or. server_index > manager%num_servers) return
1031 if (.not. manager%servers(server_index)%initialized) return
1032
1033 ! Convert filename to URI
1034 uri = "file://" // trim(filename)
1035
1036 ! Create rename request
1037 msg = create_rename_request(uri, line, character, new_name)
1038 request_id = msg%id
1039
1040 call send_request(manager, server_index, msg, callback)
1041 end function request_rename
1042
1043 ! Request workspace symbols
1044 function request_workspace_symbols(manager, server_index, query, callback) result(request_id)
1045 use lsp_protocol_module, only: create_workspace_symbols_request
1046 type(lsp_manager_t), intent(inout) :: manager
1047 integer, intent(in) :: server_index
1048 character(len=*), intent(in) :: query
1049 procedure(response_callback), optional :: callback
1050 integer :: request_id
1051 type(lsp_message_t) :: msg
1052
1053 request_id = -1
1054 if (server_index < 1 .or. server_index > manager%num_servers) return
1055 if (.not. manager%servers(server_index)%initialized) return
1056 if (.not. manager%servers(server_index)%supports_workspace_symbols) return
1057
1058 ! Create workspace symbols request
1059 msg = create_workspace_symbols_request(query)
1060 request_id = msg%id
1061
1062 call send_request(manager, server_index, msg, callback)
1063 end function request_workspace_symbols
1064
1065 ! Set the diagnostics notification handler
1066 subroutine set_diagnostics_handler(manager, handler)
1067 type(lsp_manager_t), intent(inout) :: manager
1068 procedure(diagnostics_callback) :: handler
1069
1070 manager%diagnostics_handler => handler
1071 end subroutine set_diagnostics_handler
1072
1073 end module lsp_server_manager_module