Fortran · 23136 bytes Raw Blame History
1 module lsp_protocol_module
2 ! Core LSP protocol implementation
3 use iso_fortran_env, only: int32, int64, real64, error_unit
4 use json_module
5 implicit none
6 private
7
8 public :: lsp_message_t
9 public :: lsp_request_t, lsp_response_t, lsp_notification_t
10 public :: create_initialize_request
11 public :: create_initialized_notification
12 public :: create_did_open_notification
13 public :: create_did_change_notification
14 public :: create_did_save_notification
15 public :: create_did_close_notification
16 public :: create_completion_request
17 public :: create_hover_request
18 public :: create_definition_request
19 public :: create_references_request
20 public :: create_code_action_request
21 public :: create_document_symbols_request
22 public :: create_signature_help_request
23 public :: create_formatting_request
24 public :: create_rename_request
25 public :: create_workspace_symbols_request
26 public :: parse_lsp_message
27 public :: format_json_rpc
28
29 ! LSP message types
30 type :: lsp_message_t
31 character(len=:), allocatable :: jsonrpc
32 integer :: id = -1 ! -1 for notifications
33 character(len=:), allocatable :: method
34 type(json_value_t) :: params
35 type(json_value_t) :: result
36 type(json_value_t) :: error
37 logical :: is_request = .false.
38 logical :: is_response = .false.
39 logical :: is_notification = .false.
40 end type lsp_message_t
41
42 type :: lsp_request_t
43 integer :: id
44 character(len=:), allocatable :: method
45 type(json_value_t) :: params
46 end type lsp_request_t
47
48 type :: lsp_response_t
49 integer :: id
50 type(json_value_t) :: result
51 type(json_value_t) :: error
52 end type lsp_response_t
53
54 type :: lsp_notification_t
55 character(len=:), allocatable :: method
56 type(json_value_t) :: params
57 end type lsp_notification_t
58
59 ! Request ID counter
60 integer :: next_request_id = 1
61
62 contains
63
64 function get_next_request_id() result(id)
65 integer :: id
66 id = next_request_id
67 next_request_id = next_request_id + 1
68 end function get_next_request_id
69
70 function make_absolute_path(path) result(abs_path)
71 character(len=*), intent(in) :: path
72 character(len=:), allocatable :: abs_path
73 character(len=4096) :: cwd
74 integer :: status
75
76 ! Check if already absolute
77 if (len_trim(path) > 0 .and. path(1:1) == '/') then
78 abs_path = trim(path)
79 else
80 ! Get current working directory and prepend
81 call getcwd(cwd, status)
82 if (status == 0) then
83 abs_path = trim(cwd) // "/" // trim(path)
84 else
85 abs_path = trim(path)
86 end if
87 end if
88 end function make_absolute_path
89
90 function create_initialize_request(process_id, root_path, client_name) result(msg)
91 integer, intent(in) :: process_id
92 character(len=*), intent(in) :: root_path
93 character(len=*), intent(in) :: client_name
94 type(lsp_message_t) :: msg
95 type(json_value_t) :: params, client_info, capabilities
96 type(json_value_t) :: text_document, completion, hover
97 type(json_value_t) :: definition, references, doc_symbols
98 type(json_value_t) :: workspace, formatting
99 character(len=:), allocatable :: abs_root_path
100
101 msg%jsonrpc = "2.0"
102 msg%id = get_next_request_id()
103 msg%method = "initialize"
104 msg%is_request = .true.
105
106 ! Convert root_path to absolute path
107 abs_root_path = make_absolute_path(root_path)
108
109 params = json_create_object()
110 call json_add_number(params, "processId", real(process_id, real64))
111 call json_add_string(params, "rootPath", abs_root_path)
112 call json_add_string(params, "rootUri", "file://" // abs_root_path)
113
114 ! Client info
115 client_info = json_create_object()
116 call json_add_string(client_info, "name", client_name)
117 call json_add_string(client_info, "version", "0.1.0")
118 call json_add_object(params, "clientInfo", client_info)
119
120 ! Client capabilities
121 capabilities = json_create_object()
122
123 ! Text document capabilities
124 text_document = json_create_object()
125
126 ! Completion
127 completion = json_create_object()
128 call json_add_bool(completion, "dynamicRegistration", .false.)
129 call json_add_bool(completion, "contextSupport", .true.)
130 call json_add_object(text_document, "completion", completion)
131
132 ! Hover
133 hover = json_create_object()
134 call json_add_bool(hover, "dynamicRegistration", .false.)
135 call json_add_object(text_document, "hover", hover)
136
137 ! Definition
138 definition = json_create_object()
139 call json_add_bool(definition, "dynamicRegistration", .false.)
140 call json_add_bool(definition, "linkSupport", .true.)
141 call json_add_object(text_document, "definition", definition)
142
143 ! References
144 references = json_create_object()
145 call json_add_bool(references, "dynamicRegistration", .false.)
146 call json_add_object(text_document, "references", references)
147
148 ! Document symbols
149 doc_symbols = json_create_object()
150 call json_add_bool(doc_symbols, "dynamicRegistration", .false.)
151 call json_add_object(text_document, "documentSymbol", doc_symbols)
152
153 ! Formatting
154 formatting = json_create_object()
155 call json_add_bool(formatting, "dynamicRegistration", .false.)
156 call json_add_object(text_document, "formatting", formatting)
157
158 call json_add_object(capabilities, "textDocument", text_document)
159
160 ! Workspace capabilities
161 workspace = json_create_object()
162 call json_add_bool(workspace, "applyEdit", .true.)
163 ! workspaceEdit must be an object, not a boolean
164 block
165 type(json_value_t) :: workspace_edit
166 workspace_edit = json_create_object()
167 call json_add_bool(workspace_edit, "documentChanges", .true.)
168 call json_add_object(workspace, "workspaceEdit", workspace_edit)
169 end block
170 call json_add_object(capabilities, "workspace", workspace)
171
172 call json_add_object(params, "capabilities", capabilities)
173
174 msg%params = params
175 end function create_initialize_request
176
177 function create_initialized_notification() result(msg)
178 type(lsp_message_t) :: msg
179 type(json_value_t) :: params
180
181 msg%jsonrpc = "2.0"
182 msg%method = "initialized"
183 msg%is_notification = .true.
184
185 params = json_create_object()
186 msg%params = params
187 end function create_initialized_notification
188
189 function create_did_open_notification(uri, language_id, version, text) result(msg)
190 character(len=*), intent(in) :: uri, language_id, text
191 integer, intent(in) :: version
192 type(lsp_message_t) :: msg
193 type(json_value_t) :: params, text_document
194
195 msg%jsonrpc = "2.0"
196 msg%method = "textDocument/didOpen"
197 msg%is_notification = .true.
198
199 params = json_create_object()
200 text_document = json_create_object()
201
202 call json_add_string(text_document, "uri", uri)
203 call json_add_string(text_document, "languageId", language_id)
204 call json_add_number(text_document, "version", real(version, real64))
205 call json_add_string(text_document, "text", text)
206
207 call json_add_object(params, "textDocument", text_document)
208 msg%params = params
209 end function create_did_open_notification
210
211 function create_did_change_notification(uri, version, text) result(msg)
212 character(len=*), intent(in) :: uri, text
213 integer, intent(in) :: version
214 type(lsp_message_t) :: msg
215 type(json_value_t) :: params, text_document, changes, change
216
217 msg%jsonrpc = "2.0"
218 msg%method = "textDocument/didChange"
219 msg%is_notification = .true.
220
221 params = json_create_object()
222
223 text_document = json_create_object()
224 call json_add_string(text_document, "uri", uri)
225 call json_add_number(text_document, "version", real(version, real64))
226 call json_add_object(params, "textDocument", text_document)
227
228 changes = json_create_array()
229 change = json_create_object()
230 call json_add_string(change, "text", text)
231 call json_array_add_element(changes, change)
232 call json_add_array(params, "contentChanges", changes)
233
234 msg%params = params
235 end function create_did_change_notification
236
237 function create_did_save_notification(uri, text) result(msg)
238 character(len=*), intent(in) :: uri
239 character(len=*), intent(in), optional :: text
240 type(lsp_message_t) :: msg
241 type(json_value_t) :: params, text_document
242
243 msg%jsonrpc = "2.0"
244 msg%method = "textDocument/didSave"
245 msg%is_notification = .true.
246
247 params = json_create_object()
248 text_document = json_create_object()
249 call json_add_string(text_document, "uri", uri)
250 call json_add_object(params, "textDocument", text_document)
251
252 if (present(text)) then
253 call json_add_string(params, "text", text)
254 end if
255
256 msg%params = params
257 end function create_did_save_notification
258
259 function create_did_close_notification(uri) result(msg)
260 character(len=*), intent(in) :: uri
261 type(lsp_message_t) :: msg
262 type(json_value_t) :: params, text_document
263
264 msg%jsonrpc = "2.0"
265 msg%method = "textDocument/didClose"
266 msg%is_notification = .true.
267
268 params = json_create_object()
269 text_document = json_create_object()
270 call json_add_string(text_document, "uri", uri)
271 call json_add_object(params, "textDocument", text_document)
272
273 msg%params = params
274 end function create_did_close_notification
275
276 function create_completion_request(uri, line, character) result(msg)
277 character(len=*), intent(in) :: uri
278 integer, intent(in) :: line, character
279 type(lsp_message_t) :: msg
280 type(json_value_t) :: params, text_document, position
281
282 msg%jsonrpc = "2.0"
283 msg%id = get_next_request_id()
284 msg%method = "textDocument/completion"
285 msg%is_request = .true.
286
287 params = json_create_object()
288
289 text_document = json_create_object()
290 call json_add_string(text_document, "uri", uri)
291 call json_add_object(params, "textDocument", text_document)
292
293 position = json_create_object()
294 call json_add_number(position, "line", real(line, real64))
295 call json_add_number(position, "character", real(character, real64))
296 call json_add_object(params, "position", position)
297
298 msg%params = params
299 end function create_completion_request
300
301 function create_hover_request(uri, line, character) result(msg)
302 character(len=*), intent(in) :: uri
303 integer, intent(in) :: line, character
304 type(lsp_message_t) :: msg
305 type(json_value_t) :: params, text_document, position
306
307 msg%jsonrpc = "2.0"
308 msg%id = get_next_request_id()
309 msg%method = "textDocument/hover"
310 msg%is_request = .true.
311
312 params = json_create_object()
313
314 text_document = json_create_object()
315 call json_add_string(text_document, "uri", uri)
316 call json_add_object(params, "textDocument", text_document)
317
318 position = json_create_object()
319 call json_add_number(position, "line", real(line, real64))
320 call json_add_number(position, "character", real(character, real64))
321 call json_add_object(params, "position", position)
322
323 msg%params = params
324 end function create_hover_request
325
326 function create_definition_request(uri, line, character) result(msg)
327 character(len=*), intent(in) :: uri
328 integer, intent(in) :: line, character
329 type(lsp_message_t) :: msg
330 type(json_value_t) :: params, text_document, position
331
332 msg%jsonrpc = "2.0"
333 msg%id = get_next_request_id()
334 msg%method = "textDocument/definition"
335 msg%is_request = .true.
336
337 params = json_create_object()
338
339 text_document = json_create_object()
340 call json_add_string(text_document, "uri", uri)
341 call json_add_object(params, "textDocument", text_document)
342
343 position = json_create_object()
344 call json_add_number(position, "line", real(line, real64))
345 call json_add_number(position, "character", real(character, real64))
346 call json_add_object(params, "position", position)
347
348 msg%params = params
349 end function create_definition_request
350
351 function create_references_request(uri, line, character, include_declaration) result(msg)
352 character(len=*), intent(in) :: uri
353 integer, intent(in) :: line, character
354 logical, intent(in) :: include_declaration
355 type(lsp_message_t) :: msg
356 type(json_value_t) :: params, text_document, position, context
357
358 msg%jsonrpc = "2.0"
359 msg%id = get_next_request_id()
360 msg%method = "textDocument/references"
361 msg%is_request = .true.
362
363 params = json_create_object()
364
365 text_document = json_create_object()
366 call json_add_string(text_document, "uri", uri)
367 call json_add_object(params, "textDocument", text_document)
368
369 position = json_create_object()
370 call json_add_number(position, "line", real(line, real64))
371 call json_add_number(position, "character", real(character, real64))
372 call json_add_object(params, "position", position)
373
374 context = json_create_object()
375 call json_add_bool(context, "includeDeclaration", include_declaration)
376 call json_add_object(params, "context", context)
377
378 msg%params = params
379 end function create_references_request
380
381 function create_code_action_request(uri, start_line, start_char, end_line, end_char, diagnostics) result(msg)
382 character(len=*), intent(in) :: uri
383 integer, intent(in) :: start_line, start_char, end_line, end_char
384 type(json_value_t), intent(in), optional :: diagnostics
385 type(lsp_message_t) :: msg
386 type(json_value_t) :: params, text_document, range_obj, start_pos, end_pos, context
387 type(json_value_t) :: empty_array
388
389 msg%jsonrpc = "2.0"
390 msg%id = get_next_request_id()
391 msg%method = "textDocument/codeAction"
392 msg%is_request = .true.
393
394 params = json_create_object()
395
396 ! Text document
397 text_document = json_create_object()
398 call json_add_string(text_document, "uri", uri)
399 call json_add_object(params, "textDocument", text_document)
400
401 ! Range
402 range_obj = json_create_object()
403 start_pos = json_create_object()
404 call json_add_number(start_pos, "line", real(start_line, real64))
405 call json_add_number(start_pos, "character", real(start_char, real64))
406 call json_add_object(range_obj, "start", start_pos)
407
408 end_pos = json_create_object()
409 call json_add_number(end_pos, "line", real(end_line, real64))
410 call json_add_number(end_pos, "character", real(end_char, real64))
411 call json_add_object(range_obj, "end", end_pos)
412 call json_add_object(params, "range", range_obj)
413
414 ! Context with diagnostics and code action kinds
415 context = json_create_object()
416 if (present(diagnostics)) then
417 call json_add_array(context, "diagnostics", diagnostics)
418 else
419 empty_array = json_create_array()
420 call json_add_array(context, "diagnostics", empty_array)
421 end if
422
423 ! Note: We don't filter by 'only' to get all available actions from Ruff
424
425 call json_add_object(params, "context", context)
426
427 msg%params = params
428 end function create_code_action_request
429
430 function create_document_symbols_request(uri) result(msg)
431 character(len=*), intent(in) :: uri
432 type(lsp_message_t) :: msg
433 type(json_value_t) :: params, text_document
434
435 msg%jsonrpc = "2.0"
436 msg%id = get_next_request_id()
437 msg%method = "textDocument/documentSymbol"
438 msg%is_request = .true.
439
440 params = json_create_object()
441 text_document = json_create_object()
442 call json_add_string(text_document, "uri", uri)
443 call json_add_object(params, "textDocument", text_document)
444
445 msg%params = params
446 end function create_document_symbols_request
447
448 function create_signature_help_request(uri, line, character) result(msg)
449 character(len=*), intent(in) :: uri
450 integer, intent(in) :: line, character ! 0-based LSP positions
451 type(lsp_message_t) :: msg
452 type(json_value_t) :: params, text_document, position
453
454 msg%jsonrpc = "2.0"
455 msg%id = get_next_request_id()
456 msg%method = "textDocument/signatureHelp"
457 msg%is_request = .true.
458
459 params = json_create_object()
460
461 text_document = json_create_object()
462 call json_add_string(text_document, "uri", uri)
463 call json_add_object(params, "textDocument", text_document)
464
465 position = json_create_object()
466 call json_add_number(position, "line", real(line, 8))
467 call json_add_number(position, "character", real(character, 8))
468 call json_add_object(params, "position", position)
469
470 msg%params = params
471 end function create_signature_help_request
472
473 function create_formatting_request(uri, tab_size, insert_spaces) result(msg)
474 character(len=*), intent(in) :: uri
475 integer, intent(in) :: tab_size
476 logical, intent(in) :: insert_spaces
477 type(lsp_message_t) :: msg
478 type(json_value_t) :: params, text_document, options
479
480 msg%jsonrpc = "2.0"
481 msg%id = get_next_request_id()
482 msg%method = "textDocument/formatting"
483 msg%is_request = .true.
484
485 params = json_create_object()
486
487 text_document = json_create_object()
488 call json_add_string(text_document, "uri", uri)
489 call json_add_object(params, "textDocument", text_document)
490
491 options = json_create_object()
492 call json_add_number(options, "tabSize", real(tab_size, real64))
493 call json_add_bool(options, "insertSpaces", insert_spaces)
494 call json_add_object(params, "options", options)
495
496 msg%params = params
497 end function create_formatting_request
498
499 function create_rename_request(uri, line, character, new_name) result(msg)
500 character(len=*), intent(in) :: uri, new_name
501 integer, intent(in) :: line, character
502 type(lsp_message_t) :: msg
503 type(json_value_t) :: params, text_document, position
504
505 msg%jsonrpc = "2.0"
506 msg%id = get_next_request_id()
507 msg%method = "textDocument/rename"
508 msg%is_request = .true.
509
510 params = json_create_object()
511
512 text_document = json_create_object()
513 call json_add_string(text_document, "uri", uri)
514 call json_add_object(params, "textDocument", text_document)
515
516 position = json_create_object()
517 call json_add_number(position, "line", real(line, real64))
518 call json_add_number(position, "character", real(character, real64))
519 call json_add_object(params, "position", position)
520
521 call json_add_string(params, "newName", new_name)
522
523 msg%params = params
524 end function create_rename_request
525
526 function create_workspace_symbols_request(query) result(msg)
527 character(len=*), intent(in) :: query
528 type(lsp_message_t) :: msg
529 type(json_value_t) :: params
530
531 msg%jsonrpc = "2.0"
532 msg%id = get_next_request_id()
533 msg%method = "workspace/symbol"
534 msg%is_request = .true.
535
536 params = json_create_object()
537 call json_add_string(params, "query", query)
538
539 msg%params = params
540 end function create_workspace_symbols_request
541
542 function format_json_rpc(msg) result(formatted)
543 type(lsp_message_t), intent(in) :: msg
544 character(len=:), allocatable :: formatted
545 type(json_value_t) :: json_msg
546 character(len=:), allocatable :: json_str
547 character(len=20) :: len_str
548
549 json_msg = json_create_object()
550 call json_add_string(json_msg, "jsonrpc", msg%jsonrpc)
551
552 if (msg%is_request .or. msg%is_response) then
553 call json_add_number(json_msg, "id", real(msg%id, real64))
554 end if
555
556 if (msg%is_request .or. msg%is_notification) then
557 call json_add_string(json_msg, "method", msg%method)
558 call json_add_object(json_msg, "params", msg%params)
559 end if
560
561 if (msg%is_response) then
562 if (msg%result%value_type /= JSON_NULL) then
563 call json_add_object(json_msg, "result", msg%result)
564 end if
565 if (msg%error%value_type /= JSON_NULL) then
566 call json_add_object(json_msg, "error", msg%error)
567 end if
568 end if
569
570 json_str = json_stringify(json_msg)
571
572 ! Format as LSP message with Content-Length header
573 write(len_str, '(i0)') len(json_str)
574 formatted = "Content-Length: " // trim(len_str) // char(13)//char(10) // &
575 char(13)//char(10) // json_str
576 end function format_json_rpc
577
578 function parse_lsp_message(message) result(msg)
579 character(len=*), intent(in) :: message
580 type(lsp_message_t) :: msg
581 integer :: content_start
582 character(len=:), allocatable :: json_content
583 type(json_value_t) :: json_obj
584
585 ! Find where JSON content starts (after headers)
586 content_start = index(message, char(13)//char(10)//char(13)//char(10))
587
588 if (content_start > 0) then
589 json_content = message(content_start + 4:)
590 else
591 json_content = message
592 end if
593
594 ! Parse JSON
595 json_obj = json_parse(json_content)
596
597 ! Extract message components
598 msg%jsonrpc = json_get_string(json_obj, "jsonrpc", "2.0")
599
600 if (json_has_key(json_obj, "id")) then
601 msg%id = int(json_get_number(json_obj, "id", -1.0_real64))
602 else
603 msg%id = -1
604 end if
605
606 if (json_has_key(json_obj, "method")) then
607 msg%method = json_get_string(json_obj, "method", "")
608 if (msg%id >= 0) then
609 msg%is_request = .true.
610 else
611 msg%is_notification = .true.
612 end if
613 msg%params = json_get_object(json_obj, "params")
614 else if (msg%id >= 0) then
615 msg%is_response = .true.
616 ! Try to get result as array first (for definition, symbols, etc.), then as object
617 msg%result = json_get_array(json_obj, "result")
618 if (msg%result%value_type == JSON_NULL) then
619 msg%result = json_get_object(json_obj, "result")
620 end if
621 msg%error = json_get_object(json_obj, "error")
622 end if
623 end function parse_lsp_message
624
625 end module lsp_protocol_module