Source file lp_lsp.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
open Lplib open Extra
open Common
open Core
module F = Format
module J = Yojson.Basic
module U = Yojson.Basic.Util
let int_field name dict = U.to_int List.(assoc name dict)
let dict_field name dict = U.to_assoc List.(assoc name dict)
let list_field name dict = U.to_list List.(assoc name dict)
let string_field name dict = U.to_string List.(assoc name dict)
let oint_field name dict =
Option.map_default U.to_int 0 List.(assoc_opt name dict)
let odict_field name dict =
Option.get [] U.(to_option to_assoc
(Option.get `Null List.(assoc_opt name dict)))
module LIO = Lsp_io
module LSP = Lsp_base
let do_initialize ofmt ~id _params =
let msg = LSP.mk_reply ~id ~result:(
`Assoc ["capabilities",
`Assoc [
"textDocumentSync", `Int 1
; "documentSymbolProvider", `Bool true
; "hoverProvider", `Bool true
; "definitionProvider", `Bool true
; "codeActionProvider", `Bool false
]]) in
LIO.send_json ofmt msg
let do_shutdown ofmt ~id =
let msg = LSP.mk_reply ~id ~result:`Null in
LIO.send_json ofmt msg
let doc_table : (string, _) Hashtbl.t = Hashtbl.create 39
let completed_table : (string, Lp_doc.t) Hashtbl.t = Hashtbl.create 39
let do_check_text ofmt ~doc =
let doc, diags =
try
Lp_doc.check_text ~doc
with Common.Error.Fatal(_pos, msg) ->
let loc : Pos.pos =
{
fname = Some(doc.uri);
start_line = 0;
start_col = 0;
end_line = 0;
end_col = 0
} in
(doc, Lp_doc.mk_error ~doc loc msg)
in
Hashtbl.replace doc_table doc.uri doc;
Hashtbl.replace completed_table doc.uri doc;
LIO.send_json ofmt @@ diags
let do_change ofmt ~doc change =
let open Lp_doc in
LIO.log_error "checking file"
(doc.uri ^ " / version: " ^ (string_of_int doc.version));
let doc = { doc with text = string_field "text" change; } in
do_check_text ofmt ~doc
let do_open ofmt params =
let document = dict_field "textDocument" params in
let uri, version, text =
string_field "uri" document,
int_field "version" document,
string_field "text" document in
let doc = Lp_doc.new_doc ~uri ~text ~version in
begin match Hashtbl.find_opt doc_table uri with
| None -> ()
| Some _ ->
LIO.log_error "do_open"
("file " ^ uri ^ " not properly closed by client")
end;
Hashtbl.add doc_table uri doc;
do_check_text ofmt ~doc
let do_change ofmt params =
let document = dict_field "textDocument" params in
let uri, version =
string_field "uri" document,
int_field "version" document in
let changes = List.map U.to_assoc @@ list_field "contentChanges" params in
let doc = Hashtbl.find doc_table uri in
let doc = { doc with Lp_doc.version; } in
List.iter (do_change ofmt ~doc) changes
let do_close _ofmt params =
let document = dict_field "textDocument" params in
let doc_file = string_field "uri" document in
Hashtbl.remove doc_table doc_file
let grab_doc params =
let document = dict_field "textDocument" params in
let doc_file = string_field "uri" document in
let start_doc, end_doc =
Hashtbl.(find doc_table doc_file, find completed_table doc_file) in
doc_file, start_doc, end_doc
let mk_syminfo file (name, _path, kind, pos) : J.t =
`Assoc [
"name", `String name;
"kind", `Int kind;
"location", `Assoc [
"uri", `String file
; "range", LSP.mk_range pos
]
]
let mk_definfo file pos =
`Assoc [
"uri", `String file
; "range", LSP.mk_range pos
]
let kind_of_type tm =
let open Term in
let open Timed in
let is_undef =
Option.is_None !(tm.sym_def) && List.length !(tm.sym_rules) = 0 in
match !(tm.sym_type) with
| Vari _ ->
13
| Type | Kind | Symb _ | _ when is_undef ->
14
| _ ->
12
let do_symbols ofmt ~id params =
let file, _, doc = grab_doc params in
match doc.final with
| None ->
let msg = LSP.mk_reply ~id ~result:`Null in
LIO.send_json ofmt msg
| Some ss ->
let sym = Pure.get_symbols ss in
let sym =
Extra.StrMap.fold
(fun _ s l ->
let open Term in
Option.map_default
(fun p -> mk_syminfo file
(s.sym_name, s.sym_path, kind_of_type s, p) :: l) l s.sym_pos)
sym [] in
let msg = LSP.mk_reply ~id ~result:(`List sym) in
LIO.send_json ofmt msg
let get_docTextPosition params =
let document = dict_field "textDocument" params in
let file = string_field "uri" document in
let pos = dict_field "position" params in
let line, character = int_field "line" pos, int_field "character" pos in
file, line, character
let get_textPosition params =
let pos = dict_field "position" params in
let line, character = int_field "line" pos, int_field "character" pos in
line, character
(** [closest_before (line, pos) objs] returns the element in [objs] with
largest position which is before [(line, pos)]*)
let closest_before : int * int -> ('a * Pos.popt) list ->
('a * Pos.popt) option =
fun (line, pos) (objs: ('a * Pos.popt) list) ->
let objs =
List.filter (fun (_, objpos) ->
match objpos with
| None -> false
| Some objpos ->
let open Pos in
compare (objpos.start_line, objpos.start_col) (line, pos) <= 0)
objs
in
let closer (acc: ('a * Pos.popt) option) (obj : 'a * Pos.popt) =
let open Pos in
match (snd obj) with
| None -> acc
| Some objpos ->
match acc with
| None -> Some obj
| Some (_, accposopt) ->
match accposopt with
| None -> Some obj
| Some accpos ->
let comp =
compare (objpos.start_line, objpos.start_col)
(accpos.start_line, accpos.start_col)
in
if comp <= 0 then acc else Some obj
in
List.fold_left closer None objs
let in_range ?loc (line, pos) =
match loc with
| None -> false
| Some Pos.{start_line; start_col; end_line; end_col; _} ->
let line = line + 1 in
(compare (start_line, start_col) (line, pos)) *
(compare (end_line, end_col) (line, pos)) <= 0
let get_node_at_pos doc line pos =
let open Lp_doc in
List.find_opt (fun { ast; _ } ->
let loc = Pure.Command.get_pos ast in
let res = in_range ?loc (line,pos) in
let ls = Format.asprintf "%B l:%d p:%d / %a "
res line pos Pos.pp loc in
LIO.log_error "get_node_at_pos" ("call: "^ls);
res
) doc.Lp_doc.nodes
(** [get_first_error doc] returns the first error inferred from doc.logs *)
let get_first_error doc =
List.fold_left (fun acc b ->
let ((sev, _), bpos) = b in
if sev != 1 then acc else
match acc with
| None -> Some b
| Some ((_, _), apos) ->
let open Pos in
match apos with None -> Some b | Some apos ->
match bpos with None -> acc | Some bpos ->
if compare (apos.start_line, apos.start_col)
(bpos.start_line, bpos.start_col) <= 0 then
acc else Some b) None doc.Lp_doc.logs
let rec get_goals ~doc ~line ~pos =
let (line,pos) =
match get_first_error doc with
| Some ((_,_), Some errpos) ->
let errpos = (errpos.start_line, errpos.start_col) in
if compare errpos (line, pos) <= 0 then errpos else (line, pos)
| _ -> (line,pos)
in
let node = get_node_at_pos doc line pos in
let goals = match node with
| None -> Some([], None)
| Some n ->
closest_before (line+1, pos) n.goals in
match goals with
| None -> begin match node with
| None -> None
| Some _ -> get_goals ~doc ~line:(line-1) ~pos:0 end
| Some (v,_) -> Some v
let get_logs ~doc ~line ~pos : string =
LIO.log_error "get_logs"
(Printf.sprintf "%s:%d,%d" doc.Lp_doc.uri line pos);
let log_to_str ((sev, log), posopt) =
let pos_str =
match posopt with
| None -> "None"
| Some Pos.{start_line; start_col; _} ->
Printf.sprintf "(%d, %d)" start_line start_col
in
let log_str =
let len = String.length log in
Printf.sprintf "length: %d | %s" len (String.sub log 0 (min 30 len))in
Format.asprintf "element(severity:%d): %s -> %s\n " sev pos_str log_str
in
Lsp_io.log_error "get_logs"
(List.fold_left (^) "\n" (List.map log_to_str doc.Lp_doc.logs));
let line = line+1 in
let end_limit =
match get_first_error doc with
| Some ((_,_), Some errpos) ->
let errpos = (errpos.start_line, errpos.start_col) in
if compare errpos (line, pos) <= 0 then errpos else (line, pos)
| _ -> (line,pos)
in
let logs =
List.filter_map
(fun ((_,msg),loc) ->
match loc with
| Some Pos.{start_line; start_col; _}
when compare (start_line,start_col) end_limit <= 0 -> Some msg
| _ -> None)
doc.Lp_doc.logs
in
String.concat "" logs
let do_goals ofmt ~id params =
let uri, line, pos = get_docTextPosition params in
let doc = Hashtbl.find completed_table uri in
let goals = get_goals ~doc ~line ~pos in
let logs = get_logs ~doc ~line ~pos in
let result = LSP.json_of_goals goals ~logs in
let msg = LSP.mk_reply ~id ~result in
LIO.send_json ofmt msg
let msg_fail hdr msg =
LIO.log_error hdr msg;
failwith msg
let get_symbol : Range.point ->
('a * 'b) RangeMap.t -> ('b * Range.t) option
= fun pos doc ->
let open RangeMap in
match (find pos doc) with
| None -> None
| Some(interval, (_, token)) -> Some (token, interval)
let do_definition ofmt ~id params =
let _, _, doc = grab_doc params in
match doc.final with
| None ->
let msg = LSP.mk_reply ~id ~result:`Null in
LIO.send_json ofmt msg
| Some ss ->
let ln, pos = get_textPosition params in
let pt = Range.make_point (ln + 1) pos in
let sym_target =
match get_symbol pt doc.map with
| None -> "No symbol found"
| Some(token, _) -> token
in
LIO.log_error "token map" (RangeMap.to_string snd doc.map);
LIO.log_error "do_definition" sym_target;
let sym = Pure.get_symbols ss in
let map_pp : string =
Extra.StrMap.bindings sym
|> List.map (fun (key, sym) ->
Format.asprintf "{%s} / %s: @[%a@]"
key sym.Term.sym_name Pos.pp sym.sym_pos)
|> String.concat "\n"
in
LIO.log_error "symbol map" map_pp;
let sym_info =
match StrMap.find_opt sym_target sym with
| None -> `Null
| Some s ->
match s.sym_pos with
| None -> `Null
| Some pos ->
mk_definfo
Library.(file_of_path s.Term.sym_path ^ lp_src_extension) pos
in
let msg = LSP.mk_reply ~id ~result:sym_info in
LIO.send_json ofmt msg
let hover_symInfo ofmt ~id params =
let _, _, doc = grab_doc params in
let ln, pos = get_textPosition params in
let pt = Range.make_point (ln + 1) pos in
LIO.log_error "searched point" (Range.point_to_string pt);
let sym_target, interval =
match get_symbol pt doc.map with
| None ->
"No symbol found", (Range.make_interval pt pt)
| Some(token, range) ->
token, (Range.translate range 0 1)
in
try
let sym =
match doc.final with
| Some ss -> Pure.get_symbols ss
| None -> raise (Error.fatal_no_pos("Root state is missing
probably because new_doc has raised exception")) in
let start = Range.interval_start interval
and finish = Range.interval_end interval in
let sl, sc, fl, fc =
(Range.line start - 1),
(Range.column start - 1),
(Range.line finish - 1),
(Range.column finish - 1)
in
let s = `Assoc["line", `Int sl; "character", `Int sc] in
let f = `Assoc["line", `Int fl; "character", `Int fc] in
let range = `Assoc["start", s; "end", f] in
let map_pp : string =
Extra.StrMap.bindings sym
|> List.map (fun (key, sym) ->
Format.asprintf "{%s} / %s: @[%a@]"
key sym.Term.sym_name Pos.pp sym.sym_pos)
|> String.concat "\n"
in
LIO.log_error "symbol map" map_pp;
let sym_found =
let open Timed in
let open Term in
match StrMap.find_opt sym_target sym with
| None -> msg_fail "hover_SymInfo" "Sym not found"
| Some sym -> !(sym.sym_type)
in
let sym_type = Format.asprintf "%a" Core.Print.term sym_found in
let result : J.t =
`Assoc [ "contents", `String sym_type; "range", range ] in
let msg = LSP.mk_reply ~id ~result in
LIO.send_json ofmt msg
with _ ->
let msg = LSP.mk_reply ~id ~result:`Null in
LIO.send_json ofmt msg
let protect_dispatch p f x =
try f x
with
| exn ->
let bt = Printexc.get_backtrace () in
LIO.log_error ("[error] {"^p^"}") Printexc.(to_string exn);
LIO.log_error "[BT]" bt;
F.pp_print_flush !LIO.debug_fmt ()
let dispatch_message ofmt dict =
let id = oint_field "id" dict in
let params = odict_field "params" dict in
match string_field "method" dict with
| "initialize" ->
do_initialize ofmt ~id params
| "shutdown" ->
do_shutdown ofmt ~id
| "textDocument/documentSymbol" ->
protect_dispatch "do_symbols"
(do_symbols ofmt ~id) params
| "textDocument/hover" ->
hover_symInfo ofmt ~id params
| "textDocument/definition" ->
do_definition ofmt ~id params
| "proof/goals" ->
do_goals ofmt ~id params
| "textDocument/didOpen" ->
protect_dispatch "didOpen"
(do_open ofmt) params
| "textDocument/didChange" ->
protect_dispatch "didChange"
(do_change ofmt) params
| "textDocument/didClose" ->
protect_dispatch "didClose"
(do_close ofmt) params
| "exit" ->
exit 0
| "initialized"
| "workspace/didChangeWatchedModule" ->
()
| msg ->
LIO.log_error "no_handler" msg
let process_input ofmt (com : J.t) =
try dispatch_message ofmt (U.to_assoc com)
with
| U.Type_error (msg, obj) ->
LIO.log_object msg obj
| exn ->
let bt = Printexc.get_backtrace () in
LIO.log_error "[BT]" bt;
LIO.log_error "process_input" (Printexc.to_string exn);
let id = oint_field "id" (U.to_assoc com) in
let goals = None in
let logs = "" in
let result = LSP.json_of_goals goals ~logs in
let msg = LSP.mk_reply ~id ~result in
LIO.send_json ofmt msg
let main std log_file =
Printexc.record_backtrace true;
LSP.std_protocol := std;
let oc = F.std_formatter in
let debug_oc = open_out_gen [Open_append;Open_creat] 511 log_file in
LIO.debug_fmt := F.formatter_of_out_channel debug_oc;
let lp_fmt = F.formatter_of_buffer Lp_doc.lp_logger in
Console.out_fmt := lp_fmt;
Error.err_fmt := lp_fmt;
let rec loop () =
let com = LIO.read_request stdin in
LIO.log_object "read" com;
process_input oc com;
F.pp_print_flush lp_fmt ();
loop ()
in
try loop ()
with exn ->
let bt = Printexc.get_backtrace () in
LIO.log_error "[fatal error]" Printexc.(to_string exn);
LIO.log_error "[BT]" bt;
F.pp_print_flush !LIO.debug_fmt ();
flush_all ();
close_out debug_oc
let default_log_file : string = "/tmp/lambdapi_lsp_log.txt"