Skip to content

Commit 617df5f

Browse files
LSP: fix go-to-definition, hover, and stale session state
Go-to-definition and hover: - External symbols landed at line 1 of the target file (URI vs raw path comparison always failed). - "M.foo" resolved wrong: a local "foo" shadowed it, and with "require M as A", "A.foo" did not resolve at all. Route the qualified identifier through Sig_state.find_sym instead of the flat name map. - Hover returned a range shifted one column left (stale "column - 1" from 1-based days). - Illegal ranges reached the wire: the error fallback fabricated line-0 positions that became -1 after 1-based to 0-based conversion, and generated symbols carry virtual positions encoding their declaration order for the Dedukti export (a recursor points one column past the end of its inductive command, an inverted range). Add the Pos.file_start helper for the former, and sanitize in mk_range for the latter: floor coordinates at 0 and collapse inverted ranges to a caret at their start. - Ghost symbols (internal: unification rules, string literals) have no user-facing definition site and are skipped. Stale session state: initial_state restores Time and re-applies the opened file's package config, wiping other open documents' library mappings. A subsequent request on an older document resolved paths against the newest document's mappings. Call Pure.restore_time at the start of every affected request handler. Also: mk_definfo takes a plain filesystem path (its only caller never passes a URI); concise per-request logging in do_definition and hover_symInfo, replacing the unbounded token-map/symbol-map debug dumps; fix the misleading "Root state is missing" message in hover_symInfo.
1 parent 43778c7 commit 617df5f

10 files changed

Lines changed: 150 additions & 166 deletions

File tree

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Active Contributors (in alphabetical order)
77
- Abdelghani Alidra (2024-)
88
- Bruno Barras (2025-)
99
- Jean-Paul Bodeveix (2026-)
10+
- Ciarán Dunne (2026-)
1011

1112
Past Contributors (in alphabetical order)
1213
=========================================

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4747
- Selection of the library mapping with the longest matching prefix in
4848
`path_of_file`: with nested mappings, the computed module path could get a
4949
duplicated directory component.
50+
- LSP server: go-to-definition and hover on qualified identifiers, and session
51+
state leaking between open documents.
5052

5153
## 3.0.0 (2025-07-16)
5254

src/common/pos.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ let cat : pos -> pos -> pos = fun p1 p2 ->
3636
; end_col = p2.end_col
3737
; end_offset = p2.end_offset }
3838

39+
(** [file_start fname] is a zero-length position at the start of file [fname],
40+
used as a fallback for error reporting when no finer position is known. *)
41+
let file_start : string -> pos = fun fname ->
42+
{ fname = Some fname
43+
; start_line = 1; start_col = 0; start_offset = 0
44+
; end_line = 1; end_col = 0; end_offset = 0 }
45+
3946
(** [locate ?fname (p1,p2)] converts the pair of Lexing positions [p1,p2] and
4047
filename [fname] into a {!type:pos}. *)
4148
let locate : ?fname:string -> Lexing.position * Lexing.position -> pos =

src/lsp/lp_doc.ml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,7 @@ let new_doc ~uri ~version ~text =
148148
let path = String.sub uri 7 (String.length uri - 7) in
149149
Some(Pure.initial_state path), []
150150
with Error.Fatal(_pos, msg, err_desc) ->
151-
let loc : Pos.pos =
152-
{
153-
fname = Some(uri);
154-
start_line = 0;
155-
start_col = 0;
156-
start_offset = 0;
157-
end_line = 0;
158-
end_col = 0;
159-
end_offset = 0
160-
} in
161-
(None, [(1, msg ^ "\n" ^ err_desc), Some(loc)])
151+
(None, [(1, msg ^ "\n" ^ err_desc), Some (Pos.file_start uri)])
162152
in
163153
{ uri;
164154
text;

src/lsp/lp_lsp.ml

Lines changed: 92 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
(* Status: Very Experimental *)
1111
(************************************************************************)
1212

13-
open Lplib open Extra
13+
open Lplib
1414
open Common
1515
open Core
1616

@@ -59,17 +59,8 @@ let do_check_text ofmt ~doc =
5959
try
6060
Lp_doc.check_text ~doc
6161
with Common.Error.Fatal(_pos, msg, err_desc) ->
62-
let loc : Pos.pos =
63-
{
64-
fname = Some(doc.uri);
65-
start_line = 0;
66-
start_col = 0;
67-
start_offset = 0;
68-
end_line = 0;
69-
end_col = 0;
70-
end_offset = 0;
71-
} in
72-
(doc, Lp_doc.mk_error ~doc loc (msg ^ "\n" ^ err_desc))
62+
(doc, Lp_doc.mk_error ~doc (Pos.file_start doc.uri)
63+
(msg ^ "\n" ^ err_desc))
7364
in
7465
Hashtbl.replace doc_table doc.uri doc;
7566
Hashtbl.replace completed_table doc.uri doc;
@@ -131,9 +122,11 @@ let mk_syminfo file (name, _path, kind, pos) : J.t =
131122
]
132123
]
133124

125+
(* [file] is a plain filesystem path (as produced by
126+
[Library.file_of_path]), never a URI. *)
134127
let mk_definfo file pos =
135128
`Assoc [
136-
"uri", `String file
129+
"uri", `String ("file://" ^ file)
137130
; "range", LSP.mk_range pos
138131
]
139132

@@ -157,6 +150,7 @@ let do_symbols ofmt ~id params =
157150
let msg = LSP.mk_reply ~id ~result:`Null in
158151
LIO.send_json ofmt msg
159152
| Some ss ->
153+
Pure.restore_time ss;
160154
let sym = Pure.get_symbols ss in
161155
let sym =
162156
Extra.StrMap.fold
@@ -231,9 +225,10 @@ let get_node_at_pos doc line pos =
231225
List.find_opt (fun { ast; _ } ->
232226
let loc = Pure.Command.get_pos ast in
233227
let res = in_range ?loc (line,pos) in
234-
let ls = Format.asprintf "%B l:%d p:%d / %a"
235-
res line pos Pos.pp loc in
236-
LIO.log_error "get_node_at_pos" ("call: "^ls);
228+
(* trace, per command node, whether the query (line,pos) falls in its
229+
range [loc] (header "get_node_at_pos") *)
230+
LIO.log_error "get_node_at_pos"
231+
(Format.asprintf "%b l:%d p:%d / %a" res line pos Pos.pp loc);
237232
res
238233
) doc.Lp_doc.nodes
239234

@@ -272,24 +267,18 @@ let rec get_goals ~doc ~line ~pos =
272267
| Some (v,_) -> Some v
273268

274269
let get_logs ~doc ~line ~pos : string =
275-
(* DEBUG LOG START *)
270+
(* Log the query location and one line per document log element: its
271+
severity, source position and a 30-char message preview (header
272+
"get_logs"). *)
273+
let elt ((sev, msg), po) =
274+
let loc = match po with
275+
| Some p -> Printf.sprintf "%d,%d" p.Pos.start_line p.Pos.start_col
276+
| None -> "?" in
277+
let preview = String.sub msg 0 (min 30 (String.length msg)) in
278+
Format.asprintf "sev:%d @%s %S" sev loc preview in
276279
LIO.log_error "get_logs"
277-
(Printf.sprintf "%s:%d,%d" doc.Lp_doc.uri line pos);
278-
let log_to_str ((sev, log), posopt) =
279-
let pos_str =
280-
match posopt with
281-
| None -> "None"
282-
| Some Pos.{start_line; start_col; _} ->
283-
Printf.sprintf "(%d, %d)" start_line start_col
284-
in
285-
let log_str =
286-
let len = String.length log in
287-
Printf.sprintf "length: %d | %s" len (String.sub log 0 (min 30 len))in
288-
Format.asprintf "element(severity:%d): %s -> %s\n " sev pos_str log_str
289-
in
290-
Lsp_io.log_error "get_logs"
291-
(List.fold_left (^) "\n" (List.map log_to_str doc.Lp_doc.logs));
292-
(* DEBUG LOG END *)
280+
(Printf.sprintf "%s:%d,%d\n%s" doc.Lp_doc.uri line pos
281+
(String.concat "\n" (List.map elt doc.Lp_doc.logs)));
293282
let line = line+1 in
294283
let end_limit =
295284
match get_first_error doc with
@@ -318,20 +307,30 @@ let do_goals ofmt ~id params =
318307
let msg = LSP.mk_reply ~id ~result in
319308
LIO.send_json ofmt msg
320309

321-
let msg_fail hdr msg =
322-
LIO.log_error hdr msg;
323-
failwith msg
324310

325-
let get_symbol : Range.point ->
326-
('a * 'b) RangeMap.t -> ('b * Range.t) option
311+
let get_symbol : Range.point -> 'a RangeMap.t -> ('a * Range.t) option
327312
= fun pos doc ->
328313

329314
let open RangeMap in
330315

331316
match (find pos doc) with
332317
| None -> None
333-
| Some(interval, (_, token)) -> Some (token, interval)
334-
318+
| Some(interval, value) -> Some (value, interval)
319+
320+
(* Dump, to the LSP debug log, the two tables consulted when resolving the
321+
symbol under the cursor: the token map ("token map": each source range ->
322+
identifier), and the in-scope symbols with their declaration positions
323+
("symbol map"). Diagnostic aid for failed go-to-definition / hover. *)
324+
let log_symbol_maps doc ss =
325+
LIO.log_error "token map" (RangeMap.to_string snd doc.Lp_doc.map);
326+
let syms =
327+
Pure.get_symbols ss
328+
|> Extra.StrMap.bindings
329+
|> List.map (fun (name, s) ->
330+
Format.asprintf "%s: @[%a@]" name Pos.pp s.Term.sym_pos)
331+
|> String.concat "\n"
332+
in
333+
LIO.log_error "symbol map" syms
335334

336335
let do_definition ofmt ~id params =
337336

@@ -341,122 +340,65 @@ let do_definition ofmt ~id params =
341340
let msg = LSP.mk_reply ~id ~result:`Null in
342341
LIO.send_json ofmt msg
343342
| Some ss ->
343+
Pure.restore_time ss;
344+
log_symbol_maps doc ss;
344345
let ln, pos = get_textPosition params in
345346

346-
(* Lines send by the client start at 0 *)
347+
(* Lines sent by the client start at 0 *)
347348
let pt = Range.make_point (ln + 1) pos in
348-
let sym_target =
349-
match get_symbol pt doc.map with
350-
| None -> "No symbol found"
351-
| Some(token, _) -> token
352-
in
353-
354-
(*Some printing in the log*)
355-
LIO.log_error "token map" (RangeMap.to_string snd doc.map);
356-
LIO.log_error "do_definition" sym_target;
357-
358-
let sym = Pure.get_symbols ss in
359-
let map_pp : string =
360-
Extra.StrMap.bindings sym
361-
|> List.map (fun (key, sym) ->
362-
Format.asprintf "{%s} / %s: @[%a@]"
363-
key sym.Term.sym_name Pos.pp sym.sym_pos)
364-
|> String.concat "\n"
365-
in
366-
LIO.log_error "symbol map" map_pp;
367-
368349
let sym_info =
369-
match StrMap.find_opt sym_target sym with
370-
| None -> `Null
371-
| Some s ->
372-
match s.sym_pos with
350+
match get_symbol pt doc.map with
351+
| None ->
352+
LIO.log_error "do_definition" "no symbol at point"; `Null
353+
| Some (qid, _) ->
354+
LIO.log_error "do_definition" (snd qid);
355+
match Pure.find_sym ss qid with
373356
| None -> `Null
374-
| Some pos ->
375-
(* A JSON with the path towards the definition of the term
376-
and its position is returned
377-
/!\ : extension is fixed, only works for .lp files *)
378-
mk_definfo
379-
Library.(file_of_path s.Term.sym_path ^ lp_src_extension) pos
357+
(* Ghost symbols (internal symbols used e.g. for unification
358+
rules and string literals) have no user-facing definition
359+
site one could jump to. *)
360+
| Some s when s.Term.sym_path = Sign.Ghost.path -> `Null
361+
| Some s ->
362+
let file =
363+
Library.(file_of_path s.Term.sym_path ^ lp_src_extension) in
364+
let pos = Option.get (Pos.file_start file) s.sym_pos in
365+
mk_definfo file pos
380366
in
381367
let msg = LSP.mk_reply ~id ~result:sym_info in
382368
LIO.send_json ofmt msg
383369

384370
let hover_symInfo ofmt ~id params =
385-
386371
let _, _, doc = grab_doc params in
387372
let ln, pos = get_textPosition params in
388-
389-
(* Positions sent by the client are one line late *)
390373
let pt = Range.make_point (ln + 1) pos in
391-
LIO.log_error "searched point" (Range.point_to_string pt);
392-
393-
(* The hovered token and its start/finish positions are stored *)
394-
let sym_target, interval =
395-
match get_symbol pt doc.map with
396-
| None ->
397-
"No symbol found", (Range.make_interval pt pt)
398-
(* VSCode highlights the token properly if the interval is extended to the
399-
character next to it. This might be handled differently in other
400-
editors in the future, but it is the most practical solution for
401-
now. *)
402-
| Some(token, range) ->
403-
token, (Range.translate range 0 1)
404-
in
374+
LIO.log_error "hover_symInfo" (Range.point_to_string pt);
405375

406-
(* Some printing in the log *)
407-
(* LIO.log_error "token map" (RangeMap.to_string snd doc.map);
408-
409-
LIO.log_error "hoverSymInfo" sym_target;
410-
LIO.log_error "hoverSymInfo" (Range.interval_to_string interval); *)
376+
let send_null () =
377+
LIO.send_json ofmt (LSP.mk_reply ~id ~result:`Null)
378+
in
411379

412380
try
413-
(* The information about the tokens is stored *)
414-
let sym =
415-
match doc.final with
416-
| Some ss -> Pure.get_symbols ss
417-
| None -> raise (Error.fatal_no_pos("Root state is missing
418-
probably because new_doc has raised exception")) in
419-
420-
(* The start/finish positions are used to hover the full qualified term,
421-
not just the token *)
422-
let start = Range.interval_start interval
423-
and finish = Range.interval_end interval in
424-
425-
(* FIXME: types and typed conversion should take care of this *)
426-
let sl, sc, fl, fc =
427-
(Range.line start - 1),
428-
(Range.column start - 1),
429-
(Range.line finish - 1),
430-
(Range.column finish - 1)
431-
in
432-
433-
let s = `Assoc["line", `Int sl; "character", `Int sc] in
434-
let f = `Assoc["line", `Int fl; "character", `Int fc] in
435-
let range = `Assoc["start", s; "end", f] in
436-
437-
let map_pp : string =
438-
Extra.StrMap.bindings sym
439-
|> List.map (fun (key, sym) ->
440-
Format.asprintf "{%s} / %s: @[%a@]"
441-
key sym.Term.sym_name Pos.pp sym.sym_pos)
442-
|> String.concat "\n"
443-
in
444-
LIO.log_error "symbol map" map_pp;
445-
446-
let sym_found =
447-
match StrMap.find_opt sym_target sym with
448-
| None -> msg_fail "hover_SymInfo" "Sym not found"
449-
| Some sym -> sym
450-
in
451-
let sym_type = Format.asprintf "%a" Core.Print.sym_type sym_found in
452-
let result : J.t =
453-
`Assoc [ "contents", `String sym_type; "range", range ] in
454-
let msg = LSP.mk_reply ~id ~result in
455-
LIO.send_json ofmt msg
456-
457-
with _ ->
458-
let msg = LSP.mk_reply ~id ~result:`Null in
459-
LIO.send_json ofmt msg
381+
let ss = match doc.final with
382+
| Some ss -> ss
383+
| None ->
384+
raise (Error.fatal_no_pos "Final state is missing: the document \
385+
was never successfully loaded") in
386+
Pure.restore_time ss;
387+
log_symbol_maps doc ss;
388+
389+
match get_symbol pt doc.map with
390+
| None -> send_null ()
391+
| Some (qid, range) ->
392+
match Pure.find_sym ss qid with
393+
| None -> LIO.log_error "hover_symInfo" "Sym not found"; send_null ()
394+
| Some sym_found ->
395+
let sym_type = Format.asprintf "%a" Core.Print.sym_type sym_found in
396+
let result = `Assoc [ "contents", `String sym_type
397+
; "range", LSP.mk_range_of_interval range ] in
398+
LIO.send_json ofmt (LSP.mk_reply ~id ~result)
399+
with e ->
400+
LIO.log_error "hover_symInfo" (Printexc.to_string e);
401+
send_null ()
460402

461403
let protect_dispatch p f x =
462404
try f x
@@ -488,10 +430,12 @@ let dispatch_message ofmt dict =
488430
(do_symbols ofmt ~id) params
489431

490432
| "textDocument/hover" ->
491-
hover_symInfo ofmt ~id params
433+
(try hover_symInfo ofmt ~id params
434+
with _ -> LIO.send_json ofmt (LSP.mk_reply ~id ~result:`Null))
492435

493436
| "textDocument/definition" ->
494-
do_definition ofmt ~id params
437+
(try do_definition ofmt ~id params
438+
with _ -> LIO.send_json ofmt (LSP.mk_reply ~id ~result:`Null))
495439

496440
| "proof/goals" ->
497441
do_goals ofmt ~id params
@@ -528,13 +472,12 @@ let process_input ofmt (com : J.t) =
528472
let bt = Printexc.get_backtrace () in
529473
LIO.log_error "[BT]" bt;
530474
LIO.log_error "process_input" (Printexc.to_string exn);
531-
(*Send an "empty" answer with Null goals when exception occurs*)
532-
let id = oint_field "id" (U.to_assoc com) in
533-
let goals = None in
534-
let logs = "" in
535-
let result = LSP.json_of_goals goals ~logs in
536-
let msg = LSP.mk_reply ~id ~result in
537-
LIO.send_json ofmt msg
475+
(* Send a null reply so the client doesn't hang *)
476+
let id = oint_field "id" (U.to_assoc com) in
477+
if id <> 0 then begin
478+
let msg = LSP.mk_reply ~id ~result:`Null in
479+
LIO.send_json ofmt msg
480+
end
538481

539482
let main std log_file =
540483

src/lsp/lp_lsp.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
val default_log_file : string
1414

1515
val main : bool -> string -> unit
16+
(** [main standard_lsp log_file] starts the LSP server. *)

0 commit comments

Comments
 (0)