Fortran · 20408 bytes Raw Blame History
1 module lsp_client_module
2 ! High-level LSP client interface for fac
3 use iso_fortran_env, only: int32, int64, output_unit, error_unit
4 use json_module
5 use lsp_protocol_module
6 use lsp_server_manager_module
7 implicit none
8 private
9
10 public :: lsp_client_t
11 public :: init_lsp_client, cleanup_lsp_client
12 public :: open_document, close_document, save_document
13 public :: document_changed
14 public :: get_completions, get_hover_info
15 public :: go_to_definition, find_references
16 public :: get_document_symbols
17 public :: format_document
18 public :: rename_symbol
19 public :: get_code_actions
20 public :: process_lsp_messages
21
22 ! Document tracking
23 type :: document_info_t
24 character(len=:), allocatable :: uri
25 character(len=:), allocatable :: file_path
26 character(len=:), allocatable :: language_id
27 integer :: version = 0
28 integer :: server_index = 0 ! 0 means no server assigned
29 end type document_info_t
30
31 ! Type stubs for return types
32 type :: lsp_completion_t
33 character(len=:), allocatable :: label
34 character(len=:), allocatable :: detail
35 end type lsp_completion_t
36
37 type :: lsp_location_t
38 character(len=:), allocatable :: uri
39 integer :: line
40 integer :: column
41 end type lsp_location_t
42
43 type :: lsp_document_symbol_t
44 character(len=:), allocatable :: name
45 integer :: kind
46 end type lsp_document_symbol_t
47
48 type :: lsp_code_action_t
49 character(len=:), allocatable :: title
50 character(len=:), allocatable :: command
51 end type lsp_code_action_t
52
53 ! Main LSP client
54 type :: lsp_client_t
55 type(lsp_manager_t) :: manager
56 type(document_info_t), allocatable :: documents(:)
57 integer :: num_documents = 0
58 character(len=:), allocatable :: workspace_root
59 end type lsp_client_t
60
61 contains
62
63 subroutine init_lsp_client(client, workspace_root)
64 type(lsp_client_t), intent(out) :: client
65 character(len=*), intent(in), optional :: workspace_root
66
67 call init_lsp_manager(client%manager)
68 allocate(client%documents(0))
69 client%num_documents = 0
70
71 if (present(workspace_root)) then
72 client%workspace_root = workspace_root
73 else
74 client%workspace_root = "."
75 end if
76 end subroutine init_lsp_client
77
78 subroutine cleanup_lsp_client(client)
79 type(lsp_client_t), intent(inout) :: client
80 integer :: i
81
82 ! Close all documents
83 do i = 1, client%num_documents
84 call close_document(client, client%documents(i)%file_path)
85 end do
86
87 ! Clean up manager
88 call cleanup_lsp_manager(client%manager)
89
90 if (allocated(client%documents)) deallocate(client%documents)
91 client%num_documents = 0
92 end subroutine cleanup_lsp_client
93
94 subroutine open_document(client, file_path, content, language_id)
95 type(lsp_client_t), intent(inout) :: client
96 character(len=*), intent(in) :: file_path, content
97 character(len=*), intent(in), optional :: language_id
98 type(document_info_t), allocatable :: new_documents(:)
99 type(lsp_message_t) :: msg
100 character(len=:), allocatable :: lang_id, uri
101 integer :: i, server_index
102
103 ! Check if already open
104 do i = 1, client%num_documents
105 if (client%documents(i)%file_path == file_path) then
106 ! Already open, just update
107 client%documents(i)%version = client%documents(i)%version + 1
108 return
109 end if
110 end do
111
112 ! Determine language ID
113 if (present(language_id)) then
114 lang_id = language_id
115 else
116 lang_id = detect_language_from_extension(file_path)
117 end if
118
119 ! Get or start server for this language
120 server_index = get_or_start_server(client%manager, lang_id, client%workspace_root)
121 if (server_index <= 0) return
122
123 ! Build URI
124 uri = file_path_to_uri(file_path)
125
126 ! Add to documents array
127 allocate(new_documents(client%num_documents + 1))
128 if (client%num_documents > 0) then
129 new_documents(1:client%num_documents) = client%documents
130 end if
131
132 new_documents(client%num_documents + 1)%uri = uri
133 new_documents(client%num_documents + 1)%file_path = file_path
134 new_documents(client%num_documents + 1)%language_id = lang_id
135 new_documents(client%num_documents + 1)%version = 1
136 new_documents(client%num_documents + 1)%server_index = server_index
137
138 deallocate(client%documents)
139 client%documents = new_documents
140 client%num_documents = client%num_documents + 1
141
142 ! Send didOpen notification
143 if (client%manager%servers(server_index)%initialized) then
144 msg = create_did_open_notification(uri, lang_id, 1, content)
145 call send_notification(client%manager%servers(server_index), msg)
146 end if
147 end subroutine open_document
148
149 subroutine close_document(client, file_path)
150 type(lsp_client_t), intent(inout) :: client
151 character(len=*), intent(in) :: file_path
152 type(lsp_message_t) :: msg
153 integer :: i, j
154
155 do i = 1, client%num_documents
156 if (client%documents(i)%file_path == file_path) then
157 ! Send didClose notification
158 if (client%documents(i)%server_index > 0) then
159 if (client%manager%servers(client%documents(i)%server_index)%initialized) then
160 msg = create_did_close_notification(client%documents(i)%uri)
161 call send_notification(client%manager%servers(client%documents(i)%server_index), msg)
162 end if
163 end if
164
165 ! Remove from array
166 do j = i, client%num_documents - 1
167 client%documents(j) = client%documents(j + 1)
168 end do
169 client%num_documents = client%num_documents - 1
170 exit
171 end if
172 end do
173 end subroutine close_document
174
175 subroutine save_document(client, file_path, content)
176 type(lsp_client_t), intent(inout) :: client
177 character(len=*), intent(in) :: file_path
178 character(len=*), intent(in), optional :: content
179 type(lsp_message_t) :: msg
180 integer :: i
181
182 do i = 1, client%num_documents
183 if (client%documents(i)%file_path == file_path) then
184 if (client%documents(i)%server_index > 0) then
185 if (client%manager%servers(client%documents(i)%server_index)%initialized) then
186 if (present(content)) then
187 msg = create_did_save_notification(client%documents(i)%uri, content)
188 else
189 msg = create_did_save_notification(client%documents(i)%uri)
190 end if
191 call send_notification(client%manager%servers(client%documents(i)%server_index), msg)
192 end if
193 end if
194 exit
195 end if
196 end do
197 end subroutine save_document
198
199 subroutine document_changed(client, file_path, content)
200 type(lsp_client_t), intent(inout) :: client
201 character(len=*), intent(in) :: file_path, content
202 type(lsp_message_t) :: msg
203 integer :: i
204
205 do i = 1, client%num_documents
206 if (client%documents(i)%file_path == file_path) then
207 client%documents(i)%version = client%documents(i)%version + 1
208
209 if (client%documents(i)%server_index > 0) then
210 if (client%manager%servers(client%documents(i)%server_index)%initialized) then
211 msg = create_did_change_notification(client%documents(i)%uri, &
212 client%documents(i)%version, content)
213 call send_notification(client%manager%servers(client%documents(i)%server_index), msg)
214 end if
215 end if
216 exit
217 end if
218 end do
219 end subroutine document_changed
220
221 function get_completions(client, file_path, line, column) result(completions)
222 type(lsp_client_t), intent(inout) :: client
223 character(len=*), intent(in) :: file_path
224 integer, intent(in) :: line, column
225 type(lsp_completion_t), allocatable :: completions(:)
226 type(lsp_message_t) :: msg
227 integer :: i
228
229 allocate(completions(0))
230
231 do i = 1, client%num_documents
232 if (client%documents(i)%file_path == file_path) then
233 if (client%documents(i)%server_index > 0) then
234 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
235 client%manager%servers(client%documents(i)%server_index)%supports_completion) then
236 msg = create_completion_request(client%documents(i)%uri, &
237 line - 1, column - 1) ! LSP is 0-based
238 call send_request(client%manager, client%documents(i)%server_index, msg)
239 ! TODO: Wait for and process response
240 end if
241 end if
242 exit
243 end if
244 end do
245 end function get_completions
246
247 function get_hover_info(client, file_path, line, column) result(info)
248 type(lsp_client_t), intent(inout) :: client
249 character(len=*), intent(in) :: file_path
250 integer, intent(in) :: line, column
251 character(len=:), allocatable :: info
252 type(lsp_message_t) :: msg
253 integer :: i
254
255 info = ""
256
257 do i = 1, client%num_documents
258 if (client%documents(i)%file_path == file_path) then
259 if (client%documents(i)%server_index > 0) then
260 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
261 client%manager%servers(client%documents(i)%server_index)%supports_hover) then
262 msg = create_hover_request(client%documents(i)%uri, &
263 line - 1, column - 1) ! LSP is 0-based
264 call send_request(client%manager, client%documents(i)%server_index, msg)
265 ! TODO: Wait for and process response
266 end if
267 end if
268 exit
269 end if
270 end do
271 end function get_hover_info
272
273 function go_to_definition(client, file_path, line, column) result(location)
274 type(lsp_client_t), intent(inout) :: client
275 character(len=*), intent(in) :: file_path
276 integer, intent(in) :: line, column
277 type(lsp_location_t) :: location
278 type(lsp_message_t) :: msg
279 integer :: i
280
281 location%uri = ""
282
283 do i = 1, client%num_documents
284 if (client%documents(i)%file_path == file_path) then
285 if (client%documents(i)%server_index > 0) then
286 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
287 client%manager%servers(client%documents(i)%server_index)%supports_definition) then
288 msg = create_definition_request(client%documents(i)%uri, &
289 line - 1, column - 1) ! LSP is 0-based
290 call send_request(client%manager, client%documents(i)%server_index, msg)
291 ! TODO: Wait for and process response
292 end if
293 end if
294 exit
295 end if
296 end do
297 end function go_to_definition
298
299 function find_references(client, file_path, line, column, include_declaration) result(locations)
300 type(lsp_client_t), intent(inout) :: client
301 character(len=*), intent(in) :: file_path
302 integer, intent(in) :: line, column
303 logical, intent(in), optional :: include_declaration
304 type(lsp_location_t), allocatable :: locations(:)
305 type(lsp_message_t) :: msg
306 logical :: include_decl
307 integer :: i
308
309 allocate(locations(0))
310 include_decl = .true.
311 if (present(include_declaration)) include_decl = include_declaration
312
313 do i = 1, client%num_documents
314 if (client%documents(i)%file_path == file_path) then
315 if (client%documents(i)%server_index > 0) then
316 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
317 client%manager%servers(client%documents(i)%server_index)%supports_references) then
318 msg = create_references_request(client%documents(i)%uri, &
319 line - 1, column - 1, include_decl)
320 call send_request(client%manager, client%documents(i)%server_index, msg)
321 ! TODO: Wait for and process response
322 end if
323 end if
324 exit
325 end if
326 end do
327 end function find_references
328
329 function get_document_symbols(client, file_path) result(symbols)
330 type(lsp_client_t), intent(inout) :: client
331 character(len=*), intent(in) :: file_path
332 type(lsp_document_symbol_t), allocatable :: symbols(:)
333 type(lsp_message_t) :: msg
334 integer :: i
335
336 allocate(symbols(0))
337
338 do i = 1, client%num_documents
339 if (client%documents(i)%file_path == file_path) then
340 if (client%documents(i)%server_index > 0) then
341 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
342 client%manager%servers(client%documents(i)%server_index)%supports_document_symbols) then
343 msg = create_document_symbols_request(client%documents(i)%uri)
344 call send_request(client%manager, client%documents(i)%server_index, msg)
345 ! TODO: Wait for and process response
346 end if
347 end if
348 exit
349 end if
350 end do
351 end function get_document_symbols
352
353 subroutine format_document(client, file_path, tab_size, use_spaces)
354 type(lsp_client_t), intent(inout) :: client
355 character(len=*), intent(in) :: file_path
356 integer, intent(in), optional :: tab_size
357 logical, intent(in), optional :: use_spaces
358 type(lsp_message_t) :: msg
359 integer :: tabs
360 logical :: spaces
361 integer :: i
362
363 tabs = 4
364 if (present(tab_size)) tabs = tab_size
365 spaces = .true.
366 if (present(use_spaces)) spaces = use_spaces
367
368 do i = 1, client%num_documents
369 if (client%documents(i)%file_path == file_path) then
370 if (client%documents(i)%server_index > 0) then
371 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
372 client%manager%servers(client%documents(i)%server_index)%supports_formatting) then
373 msg = create_formatting_request(client%documents(i)%uri, tabs, spaces)
374 call send_request(client%manager, client%documents(i)%server_index, msg)
375 ! TODO: Wait for and process response
376 end if
377 end if
378 exit
379 end if
380 end do
381 end subroutine format_document
382
383 subroutine rename_symbol(client, file_path, line, column, new_name)
384 type(lsp_client_t), intent(inout) :: client
385 character(len=*), intent(in) :: file_path, new_name
386 integer, intent(in) :: line, column
387 type(lsp_message_t) :: msg
388 integer :: i
389
390 do i = 1, client%num_documents
391 if (client%documents(i)%file_path == file_path) then
392 if (client%documents(i)%server_index > 0) then
393 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
394 client%manager%servers(client%documents(i)%server_index)%supports_rename) then
395 msg = create_rename_request(client%documents(i)%uri, &
396 line - 1, column - 1, new_name)
397 call send_request(client%manager, client%documents(i)%server_index, msg)
398 ! TODO: Wait for and process response
399 end if
400 end if
401 exit
402 end if
403 end do
404 end subroutine rename_symbol
405
406 function get_code_actions(client, file_path, start_line, start_col, end_line, end_col) &
407 result(actions)
408 type(lsp_client_t), intent(inout) :: client
409 character(len=*), intent(in) :: file_path
410 integer, intent(in) :: start_line, start_col, end_line, end_col
411 type(lsp_code_action_t), allocatable :: actions(:)
412 type(lsp_message_t) :: msg
413 integer :: i
414
415 allocate(actions(0))
416
417 do i = 1, client%num_documents
418 if (client%documents(i)%file_path == file_path) then
419 if (client%documents(i)%server_index > 0) then
420 if (client%manager%servers(client%documents(i)%server_index)%initialized .and. &
421 client%manager%servers(client%documents(i)%server_index)%supports_code_actions) then
422 msg = create_code_action_request(client%documents(i)%uri, &
423 start_line - 1, start_col - 1, &
424 end_line - 1, end_col - 1)
425 call send_request(client%manager, client%documents(i)%server_index, msg)
426 ! TODO: Wait for and process response
427 end if
428 end if
429 exit
430 end if
431 end do
432 end function get_code_actions
433
434 subroutine process_lsp_messages(client)
435 type(lsp_client_t), intent(inout) :: client
436
437 call process_server_messages(client%manager)
438 end subroutine process_lsp_messages
439
440 ! Helper functions
441
442 function detect_language_from_extension(file_path) result(language_id)
443 character(len=*), intent(in) :: file_path
444 character(len=:), allocatable :: language_id
445 integer :: dot_pos
446 character(len=:), allocatable :: ext
447
448 dot_pos = index(file_path, '.', back=.true.)
449 if (dot_pos > 0) then
450 ext = file_path(dot_pos:)
451
452 select case(ext)
453 case('.py', '.pyw')
454 language_id = "python"
455 case('.c', '.h')
456 language_id = "c"
457 case('.cpp', '.cc', '.cxx', '.hpp', '.hxx')
458 language_id = "cpp"
459 case('.rs')
460 language_id = "rust"
461 case('.go')
462 language_id = "go"
463 case('.js', '.mjs')
464 language_id = "javascript"
465 case('.jsx')
466 language_id = "javascriptreact"
467 case('.ts', '.mts')
468 language_id = "typescript"
469 case('.tsx')
470 language_id = "typescriptreact"
471 case('.f90', '.f95', '.f03', '.f08', '.f18')
472 language_id = "fortran"
473 case('.sh', '.bash')
474 language_id = "shellscript"
475 case('.md', '.markdown')
476 language_id = "markdown"
477 case default
478 language_id = "text"
479 end select
480 else
481 language_id = "text"
482 end if
483 end function detect_language_from_extension
484
485 function file_path_to_uri(file_path) result(uri)
486 character(len=*), intent(in) :: file_path
487 character(len=:), allocatable :: uri
488
489 ! Simple conversion - should handle URL encoding properly
490 if (file_path(1:1) == '/') then
491 uri = "file://" // file_path
492 else
493 ! Relative path - should get absolute path
494 uri = "file://" // file_path ! TODO: Get absolute path
495 end if
496 end function file_path_to_uri
497
498 end module lsp_client_module