documentSymbol emits span of entire construct
[hiphop-php.git] / hphp / hack / src / client / clientLsp.ml
blobc28b06c69f59de57d063a4376c659c612cf48461
1 (**
2 * Copyright (c) 2015, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
10 open Hh_core
11 open Lsp
12 open Lsp_fmt
13 open Hh_json_helpers
15 (* All hack-specific code relating to LSP goes in here. *)
17 (* The environment for hh_client with LSP *)
18 type env = {
19 from: string; (* The source where the client was spawned from, i.e. nuclide, vim, emacs, etc. *)
20 use_ffp_autocomplete: bool; (* Flag to turn on the (experimental) FFP based autocomplete *)
21 use_enhanced_hover: bool; (* Flag to turn on enhanced hover information *)
24 (* We cache the state of the typecoverageToggle button, so that when Hack restarts,
25 dynamic view stays in sync with the button in Nuclide *)
26 let cached_toggle_state = ref false
28 (************************************************************************)
29 (** Protocol orchestration & helpers **)
30 (************************************************************************)
32 (** We have an idea of server state based on what we hear from the server:
33 When we attempt a connection, we hear hopefully hear back that it's
34 INITIALIZING, and when we eventually receive "hello" that means it's
35 HANDLING_OR_READY, i.e. either handling a message, or ready to accept one.
36 But at connection attempt, we might see that it's STOPPED, or hear from it
37 that it's DENYING_CONNECTION (typically due to rebase).
38 When the server's running normally, we sometimes here push notifications to
39 tell us that it's TYPECHECKING, or has been STOLEN by another editor.
40 At any point of communication we might hear from the server that it
41 encountered a fatal exception, i.e. shutting down the pipe, so presumably
42 it has been STOPPED. When we reattempt to connect once a second, maybe we'll
43 get a better idea. *)
44 type hh_server_state =
45 | Hh_server_stopped
46 | Hh_server_initializing
47 | Hh_server_handling_or_ready
48 | Hh_server_denying_connection
49 | Hh_server_unknown
50 | Hh_server_typechecking_local
51 | Hh_server_typechecking_global_blocking
52 | Hh_server_typechecking_global_interruptible
53 | Hh_server_stolen
54 | Hh_server_forgot
56 (** A push message from the server might come while we're waiting for a server-rpc
57 response, or while we're free. The current architecture allows us to have
58 arbitrary responses to push messages while we're free, but only a limited set
59 of responses while we're waiting for a server-rpc - e.g. we can update our
60 notion of the server_state, or send a message to the client, but we can't
61 update our own state monad. The has_* fields are ad-hoc push-specific indicators
62 of whether we've done some part of the response during the rpc. *)
63 type server_message = {
64 push: ServerCommandTypes.push;
65 has_updated_server_state: bool;
68 type server_conn = {
69 ic: Timeout.in_channel;
70 oc: out_channel;
71 pending_messages: server_message Queue.t; (* ones that arrived during current rpc *)
74 module Main_env = struct
75 type t = {
76 conn: server_conn;
77 needs_idle: bool;
78 editor_open_files: Lsp.TextDocumentItem.t SMap.t;
79 uris_with_diagnostics: SSet.t;
80 uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
81 dialog: ShowMessageRequest.t; (* "hack server is now ready" *)
82 progress: Progress.t; (* "typechecking..." *)
83 actionRequired: ActionRequired.t; (* "save any file to trigger a global recheck" *)
85 end
87 module In_init_env = struct
88 type t = {
89 conn: server_conn;
90 first_start_time: float; (* our first attempt to connect *)
91 most_recent_start_time: float; (* for subsequent retries *)
92 editor_open_files: Lsp.TextDocumentItem.t SMap.t;
93 file_edits: Hh_json.json ImmQueue.t;
94 uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
95 tail_env: Tail.env option;
96 has_reported_progress: bool;
97 dialog: ShowMessageRequest.t; (* "hack server is busy" *)
98 progress: Progress.t; (* "hh_server is initializing [naming]" *)
102 module Lost_env = struct
103 type t = {
104 p: params;
105 editor_open_files: Lsp.TextDocumentItem.t SMap.t;
106 uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
107 lock_file: string;
108 dialog: ShowMessageRequest.t; (* "hh_server stopped" *)
109 actionRequired: ActionRequired.t; (* "hh_server stopped" *)
110 progress: Progress.t; (* "hh_server monitor is waiting for a rebase to settle" *)
113 and how_to_explain_loss_to_user =
114 | Action_required of string (* explain via dialog and actionRequired *)
115 | Wait_required of string (* explain via progress *)
117 and params = {
118 explanation: how_to_explain_loss_to_user;
119 new_hh_server_state: hh_server_state;
120 start_on_click: bool; (* if user clicks Restart, do we ClientStart before reconnecting? *)
121 trigger_on_lsp: bool; (* reconnect if we receive any LSP request/notification *)
122 trigger_on_lock_file: bool; (* reconnect if lockfile is created *)
126 type state =
127 (* Pre_init: we haven't yet received the initialize request. *)
128 | Pre_init
129 (* In_init: we did respond to the initialize request, and now we're *)
130 (* waiting for a "Hello" from the server. When that comes we'll *)
131 (* request a permanent connection from the server, and process the *)
132 (* file_changes backlog, and switch to Main_loop. *)
133 | In_init of In_init_env.t
134 (* Main_loop: we have a working connection to both server and client. *)
135 | Main_loop of Main_env.t
136 (* Lost_server: someone stole the persistent connection from us. *)
137 (* We might choose to grab it back if prompted... *)
138 | Lost_server of Lost_env.t
139 (* Post_shutdown: we received a shutdown request from the client, and *)
140 (* therefore shut down our connection to the server. We can't handle *)
141 (* any more requests from the client and will close as soon as it *)
142 (* notifies us that we can exit. *)
143 | Post_shutdown
145 type on_result = result:Hh_json.json option -> state -> state
146 type on_error = code:int -> message:string -> data:Hh_json.json option -> state -> state
147 let initialize_params_ref: Lsp.Initialize.params option ref = ref None
148 let hhconfig_version: string ref = ref "[NotYetInitialized]"
149 let can_autostart_after_mismatch: bool ref = ref true
150 let callbacks_outstanding: (on_result * on_error) IdMap.t ref = ref IdMap.empty
151 let hh_server_state: (float * hh_server_state) list ref = ref [] (* head is newest *)
153 let initialize_params_exc () : Lsp.Initialize.params =
154 match !initialize_params_ref with
155 | None -> failwith "initialize_params not yet received"
156 | Some initialize_params -> initialize_params
158 let to_stdout (json: Hh_json.json) : unit =
159 let s = (Hh_json.json_to_string json) ^ "\r\n\r\n" in
160 Http_lite.write_message stdout s
162 let get_editor_open_files (state: state) : Lsp.TextDocumentItem.t SMap.t option =
163 match state with
164 | Main_loop menv -> let open Main_env in Some menv.editor_open_files
165 | In_init ienv -> let open In_init_env in Some ienv.editor_open_files
166 | Lost_server lenv -> let open Lost_env in Some lenv.editor_open_files
167 | _ -> None
169 type event =
170 | Server_hello
171 | Server_message of server_message
172 | Client_message of Jsonrpc.message
173 | Tick (* once per second, on idle *)
175 (* Here are some exit points. *)
176 let exit_ok () = exit 0
177 let exit_fail () = exit 1
179 (* The following connection exceptions inform the main LSP event loop how to *)
180 (* respond to an exception: was the exception a connection-related exception *)
181 (* (one of these) or did it arise during other logic (not one of these)? Can *)
182 (* we report the exception to the LSP client? Can we continue handling *)
183 (* further LSP messages or must we quit? If we quit, can we do so immediately *)
184 (* or must we delay? -- Separately, they also help us marshal callstacks *)
185 (* across daemon- and process-boundaries. *)
186 exception Client_fatal_connection_exception of Marshal_tools.remote_exception_data
187 exception Client_recoverable_connection_exception of Marshal_tools.remote_exception_data
188 exception Server_fatal_connection_exception of Marshal_tools.remote_exception_data
189 exception Server_nonfatal_exception of Marshal_tools.remote_exception_data
192 let state_to_string (state: state) : string =
193 match state with
194 | Pre_init -> "Pre_init"
195 | In_init _ienv -> "In_init"
196 | Main_loop _menv -> "Main_loop"
197 | Lost_server _lenv -> "Lost_server"
198 | Post_shutdown -> "Post_shutdown"
200 let hh_server_state_to_string (hh_server_state: hh_server_state) : string =
201 match hh_server_state with
202 | Hh_server_denying_connection -> "hh_server denying connection"
203 | Hh_server_initializing -> "hh_server initializing"
204 | Hh_server_stopped -> "hh_server stopped"
205 | Hh_server_stolen -> "hh_server stolen"
206 | Hh_server_typechecking_local -> "hh_server typechecking (local)"
207 | Hh_server_typechecking_global_blocking -> "hh_server typechecking (global, blocking)"
208 | Hh_server_typechecking_global_interruptible -> "hh_server typechecking (global, interruptible)"
209 | Hh_server_handling_or_ready -> "hh_server ready"
210 | Hh_server_unknown -> "hh_server unknown state"
211 | Hh_server_forgot -> "hh_server forgotten state"
213 (** We keep a log of server state over the past 2mins. When adding a new server
214 state: if this state is the same as the current one, then ignore it. Also,
215 retain only states younger than 2min plus the first one older than 2min.
216 Newest state is at head of list. *)
217 let set_hh_server_state (new_hh_server_state: hh_server_state) : unit =
218 let new_time = Unix.gettimeofday () in
219 let rec retain rest = match rest with
220 | [] -> []
221 | (time, state)::rest when time >= new_time -. 120.0 -> (time, state)::(retain rest)
222 | (time, state)::_rest -> (time, state)::[] (* retain only the first that's older *)
224 hh_server_state := match !hh_server_state with
225 | (prev_time, prev_hh_server_state)::rest when prev_hh_server_state = new_hh_server_state ->
226 (prev_time, prev_hh_server_state)::(retain rest)
227 | rest ->
228 (new_time, new_hh_server_state)::(retain rest)
230 let get_current_hh_server_state () : hh_server_state =
231 (* current state is at head of list. *)
232 match List.hd !hh_server_state with
233 | None -> Hh_server_unknown
234 | Some (_, hh_server_state) -> hh_server_state
236 let get_older_hh_server_state (requested_time: float) : hh_server_state =
237 (* find the first item which is older than the specified time. *)
238 match List.find !hh_server_state ~f:(fun (time, _) -> time <= requested_time) with
239 | None -> Hh_server_forgot
240 | Some (_, hh_server_state) -> hh_server_state
243 let get_root_opt () : Path.t option =
244 match !initialize_params_ref with
245 | None ->
246 None (* haven't yet received initialize so we don't know *)
247 | Some initialize_params ->
248 let path = Some (Lsp_helpers.get_root initialize_params) in
249 Some (ClientArgsUtils.get_root path)
252 let read_hhconfig_version () : string =
253 match get_root_opt () with
254 | None ->
255 "[NoRoot]"
256 | Some root ->
257 let file = Filename.concat (Path.to_string root) ".hhconfig" in
259 let contents = Sys_utils.cat file in
260 let config = Config_file.parse_contents contents in
261 let version = SMap.get "version" config in
262 Option.value version ~default:"[NoVersion]"
263 with e ->
264 Printf.sprintf "[NoHhconfig:%s]" (Printexc.to_string e)
267 (* get_uris_with_unsaved_changes is the set of files for which we've *)
268 (* received didChange but haven't yet received didSave/didOpen. It is purely *)
269 (* a description of what we've heard of the editor, and is independent of *)
270 (* whether or not they've yet been synced with hh_server. *)
271 (* As it happens: in Main_loop state all these files will already have been *)
272 (* sent to hh_server; in In_init state all these files will have been queued *)
273 (* up inside file_edits ready to be sent when we receive the hello; in *)
274 (* Lost_server state they're not even queued up, and if ever we see hh_server *)
275 (* ready then we'll terminate the LSP server and trust the client to relaunch *)
276 (* us and resend a load of didOpen/didChange events. *)
277 let get_uris_with_unsaved_changes (state: state): SSet.t =
278 match state with
279 | Main_loop menv -> menv.Main_env.uris_with_unsaved_changes
280 | In_init ienv -> ienv.In_init_env.uris_with_unsaved_changes
281 | Lost_server lenv -> lenv.Lost_env.uris_with_unsaved_changes
282 | _ -> SSet.empty
285 let update_hh_server_state_if_necessary (event: event) : unit =
286 let open ServerCommandTypes in
287 let helper push = match push with
288 | BUSY_STATUS Needs_local_typecheck
289 | BUSY_STATUS Done_local_typecheck
290 | BUSY_STATUS Done_global_typecheck -> set_hh_server_state Hh_server_handling_or_ready
291 | BUSY_STATUS Doing_local_typecheck -> set_hh_server_state Hh_server_typechecking_local
292 | BUSY_STATUS Doing_global_typecheck can_interrupt -> set_hh_server_state
293 (if can_interrupt then Hh_server_typechecking_global_interruptible
294 else Hh_server_typechecking_global_blocking)
295 | NEW_CLIENT_CONNECTED -> set_hh_server_state Hh_server_stolen
296 | DIAGNOSTIC _
297 | FATAL_EXCEPTION _
298 | NONFATAL_EXCEPTION _ -> ()
300 match event with
301 | Server_message {push; has_updated_server_state=false} -> helper push
302 | _ -> ()
305 let rpc
306 (server_conn: server_conn)
307 (ref_unblocked_time: float ref)
308 (command: 'a ServerCommandTypes.t)
309 : 'a =
310 let callback () push =
311 update_hh_server_state_if_necessary (Server_message {push; has_updated_server_state=false;});
312 Queue.push {push; has_updated_server_state=true;} server_conn.pending_messages
314 let result = ServerCommand.rpc_persistent
315 (server_conn.ic, server_conn.oc) () callback command in
316 match result with
317 | Ok ((), res, start_server_handle_time) ->
318 ref_unblocked_time := start_server_handle_time;
320 | Error ((), Utils.Callstack _, ServerCommand.Remote_fatal_exception remote_e_data) ->
321 raise (Server_fatal_connection_exception remote_e_data)
322 | Error ((), Utils.Callstack _, ServerCommand.Remote_nonfatal_exception remote_e_data) ->
323 raise (Server_nonfatal_exception remote_e_data)
324 | Error ((), Utils.Callstack stack, e) ->
325 let message = Printexc.to_string e in
326 raise (Server_fatal_connection_exception { Marshal_tools.message; stack; })
329 (* Determine whether to read a message from the client (the editor) or the
330 server (hh_server), or whether neither is ready within 1s. *)
331 let get_message_source
332 (server: server_conn)
333 (client: Jsonrpc.queue)
334 : [> `From_server | `From_client | `No_source ] =
335 (* Take action on server messages in preference to client messages, because
336 server messages are very easy and quick to service (just send a message to
337 the client), while client messages require us to launch a potentially
338 long-running RPC command. *)
339 let has_server_messages = not (Queue.is_empty server.pending_messages) in
340 if has_server_messages then `From_server else
341 if Jsonrpc.has_message client then `From_client else
343 (* If no immediate messages are available, then wait up to 1 second. *)
344 let server_read_fd = Unix.descr_of_out_channel server.oc in
345 let client_read_fd = Jsonrpc.get_read_fd client in
346 let readable, _, _ = Unix.select [server_read_fd; client_read_fd] [] [] 1.0 in
347 if readable = [] then `No_source
348 else if List.mem readable server_read_fd then `From_server
349 else `From_client
352 (* A simplified version of get_message_source which only looks at client *)
353 let get_client_message_source
354 (client: Jsonrpc.queue)
355 : [> `From_client | `No_source ] =
356 if Jsonrpc.has_message client then `From_client else
357 let client_read_fd = Jsonrpc.get_read_fd client in
358 let readable, _, _ = Unix.select [client_read_fd] [] [] 1.0 in
359 if readable = [] then `No_source
360 else `From_client
363 (* Read a message unmarshaled from the server's out_channel. *)
364 let read_message_from_server (server: server_conn) : event =
365 let open ServerCommandTypes in
367 let fd = Unix.descr_of_out_channel server.oc in
368 match Marshal_tools.from_fd_with_preamble fd with
369 | Response _ ->
370 failwith "unexpected response without request"
371 | Push push -> Server_message {push; has_updated_server_state=false;}
372 | Hello -> Server_hello
373 | Ping -> failwith "unexpected ping on persistent connection"
374 with e ->
375 let message = Printexc.to_string e in
376 let stack = Printexc.get_backtrace () in
377 raise (Server_fatal_connection_exception { Marshal_tools.message; stack; })
379 (* get_next_event: picks up the next available message from either client or
380 server. The way it's implemented, at the first character of a message
381 from either client or server, we block until that message is completely
382 received. Note: if server is None (meaning we haven't yet established
383 connection with server) then we'll just block waiting for client. *)
384 let get_next_event (state: state) (client: Jsonrpc.queue) : event =
385 let from_server (server: server_conn) =
386 if Queue.is_empty server.pending_messages
387 then read_message_from_server server
388 else Server_message (Queue.take server.pending_messages)
391 let from_client (client: Jsonrpc.queue) =
392 match Jsonrpc.get_message client with
393 | `Message message -> Client_message message
394 | `Fatal_exception edata -> raise (Client_fatal_connection_exception edata)
395 | `Recoverable_exception edata -> raise (Client_recoverable_connection_exception edata)
398 match state with
399 | Main_loop { Main_env.conn; _ } | In_init { In_init_env.conn; _ } -> begin
400 match get_message_source conn client with
401 | `From_client -> from_client client
402 | `From_server -> from_server conn
403 | `No_source -> Tick
405 | _ -> begin
406 match get_client_message_source client with
407 | `From_client -> from_client client
408 | `No_source -> Tick
412 (* respond_to_error: if we threw an exception during the handling of a request,
413 report the exception to the client as the response to their request. *)
414 let respond_to_error (event: event option) (e: exn) (stack: string): unit =
415 match event with
416 | Some (Client_message c)
417 when c.Jsonrpc.kind = Jsonrpc.Request ->
418 print_error e stack |> Jsonrpc.respond to_stdout c
419 | _ ->
420 let (code, message, _original_data) = get_error_info e in
421 Lsp_helpers.telemetry_error to_stdout (Printf.sprintf "%s [%i]\n%s" message code stack)
424 (* request_showMessage: pops up a dialog *)
425 let request_showMessage
426 (on_result: on_result)
427 (on_error: on_error)
428 (type_: MessageType.t)
429 (message: string)
430 (titles: string list)
431 : ShowMessageRequest.t =
432 (* send the request *)
433 let id = NumberId (Jsonrpc.get_next_request_id ()) in
434 let actions = List.map titles ~f:(fun title -> { ShowMessageRequest.title; }) in
435 let request = ShowMessageRequestRequest { ShowMessageRequest.type_; message; actions; } in
436 let json = Lsp_fmt.print_lsp (RequestMessage (id, request)) in
437 to_stdout json;
438 (* save the callback-handlers *)
439 callbacks_outstanding := IdMap.add id (on_result, on_error) !callbacks_outstanding;
440 (* return a token *)
441 ShowMessageRequest.Some { id; }
443 (* dismiss_showMessageRequest: sends a cancellation-request for the dialog *)
444 let dismiss_showMessageRequest (dialog: ShowMessageRequest.t) : ShowMessageRequest.t =
445 begin match dialog with
446 | ShowMessageRequest.None -> ()
447 | ShowMessageRequest.Some { id; _ } ->
448 let notification = CancelRequestNotification { CancelRequest.id; } in
449 let json = Lsp_fmt.print_lsp (NotificationMessage notification) in
450 to_stdout json
451 end;
452 ShowMessageRequest.None
455 (* dismiss_ui: dismisses all dialogs, progress- and action-required *)
456 (* indicators and diagnostics in a state. *)
457 let dismiss_ui (state: state) : state =
458 let p = initialize_params_exc () in
459 match state with
460 | In_init ienv ->
461 let open In_init_env in
462 Option.iter ~f:Tail.close_env ienv.tail_env;
463 In_init { ienv with
464 tail_env = None;
465 dialog = dismiss_showMessageRequest ienv.dialog;
466 progress = Lsp_helpers.notify_progress p to_stdout ienv.progress None;
468 | Main_loop menv ->
469 let open Main_env in
470 Main_loop { menv with
471 uris_with_diagnostics = Lsp_helpers.dismiss_diagnostics to_stdout menv.uris_with_diagnostics;
472 dialog = dismiss_showMessageRequest menv.dialog;
473 progress = Lsp_helpers.notify_progress p to_stdout menv.progress None;
474 actionRequired = Lsp_helpers.notify_actionRequired p to_stdout menv.actionRequired None;
476 | Lost_server lenv ->
477 let open Lost_env in
478 Lost_server { lenv with
479 dialog = dismiss_showMessageRequest lenv.dialog;
480 actionRequired = Lsp_helpers.notify_actionRequired p to_stdout lenv.actionRequired None;
481 progress = Lsp_helpers.notify_progress p to_stdout lenv.progress None;
483 | Pre_init -> Pre_init
484 | Post_shutdown -> Post_shutdown
487 (************************************************************************)
488 (** Conversions - ad-hoc ones written as needed them, not systematic **)
489 (************************************************************************)
491 let lsp_uri_to_path = Lsp_helpers.lsp_uri_to_path
492 let path_to_lsp_uri = Lsp_helpers.path_to_lsp_uri
494 let lsp_position_to_ide (position: Lsp.position) : Ide_api_types.position =
495 { Ide_api_types.
496 line = position.line + 1;
497 column = position.character + 1;
500 let lsp_file_position_to_hack (params: Lsp.TextDocumentPositionParams.t)
501 : string * int * int =
502 let open Lsp.TextDocumentPositionParams in
503 let {Ide_api_types.line; column;} = lsp_position_to_ide params.position in
504 let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument
506 (filename, line, column)
508 let hack_pos_to_lsp_range (pos: 'a Pos.pos) : Lsp.range =
509 let line1, col1, line2, col2 = Pos.destruct_range pos in
511 start = {line = line1 - 1; character = col1 - 1;};
512 end_ = {line = line2 - 1; character = col2 - 1;};
515 let hack_pos_to_lsp_location (pos: string Pos.pos) ~(default_path: string): Lsp.Location.t =
516 let open Lsp.Location in
518 uri = path_to_lsp_uri (Pos.filename pos) ~default_path;
519 range = hack_pos_to_lsp_range pos;
522 let ide_range_to_lsp (range: Ide_api_types.range) : Lsp.range =
523 { Lsp.
524 start = { Lsp.
525 line = range.Ide_api_types.st.Ide_api_types.line - 1;
526 character = range.Ide_api_types.st.Ide_api_types.column - 1;
528 end_ = { Lsp.
529 line = range.Ide_api_types.ed.Ide_api_types.line - 1;
530 character = range.Ide_api_types.ed.Ide_api_types.column - 1;
534 let lsp_range_to_ide (range: Lsp.range) : Ide_api_types.range =
535 let open Ide_api_types in
537 st = lsp_position_to_ide range.start;
538 ed = lsp_position_to_ide range.end_;
541 let hack_symbol_definition_to_lsp_construct_location
542 (symbol: string SymbolDefinition.t)
543 ~(default_path: string)
544 : Lsp.Location.t =
545 let open SymbolDefinition in
546 hack_pos_to_lsp_location symbol.span ~default_path
548 let hack_symbol_definition_to_lsp_identifier_location
549 (symbol: string SymbolDefinition.t)
550 ~(default_path: string)
551 : Lsp.Location.t =
552 let open SymbolDefinition in
553 hack_pos_to_lsp_location symbol.pos ~default_path
555 let hack_errors_to_lsp_diagnostic
556 (filename: string)
557 (errors: Pos.absolute Errors.error_ list)
558 : PublishDiagnostics.params =
559 let open Lsp.Location in
560 let location_message (error: Pos.absolute * string) : (Lsp.Location.t * string) =
561 let (pos, message) = error in
562 let {uri; range;} = hack_pos_to_lsp_location pos ~default_path:filename in
563 ({Location.uri; range;}, message)
565 let hack_error_to_lsp_diagnostic (error: Pos.absolute Errors.error_) =
566 let all_messages = Errors.to_list error |> List.map ~f:location_message in
567 let (first_message, additional_messages) = match all_messages with
568 | hd :: tl -> (hd, tl)
569 | [] -> failwith "Expected at least one error in the error list"
571 let ({range; _}, message) = first_message in
572 let relatedLocations = additional_messages |> List.map ~f:(fun (location, message) ->
573 { PublishDiagnostics.
574 relatedLocation = location;
575 relatedMessage = message;
576 }) in
577 { Lsp.PublishDiagnostics.
578 range;
579 severity = Some PublishDiagnostics.Error;
580 code = PublishDiagnostics.IntCode (Errors.get_code error);
581 source = Some "Hack";
582 message;
583 relatedLocations;
586 (* The caller is required to give us a non-empty filename. If it is empty, *)
587 (* the following path_to_lsp_uri will fall back to the default path - which *)
588 (* is also empty - and throw, logging appropriate telemetry. *)
589 { Lsp.PublishDiagnostics.
590 uri = path_to_lsp_uri filename ~default_path:"";
591 diagnostics = List.map errors ~f:hack_error_to_lsp_diagnostic;
595 (************************************************************************)
596 (** Protocol **)
597 (************************************************************************)
599 let do_shutdown (state: state) (ref_unblocked_time: float ref): state =
600 let state = dismiss_ui state in
601 begin match state with
602 | Main_loop menv ->
603 (* In Main_loop state, we're expected to unsubscribe diagnostics and tell *)
604 (* server to disconnect so it can revert the state of its unsaved files. *)
605 let open Main_env in
606 rpc menv.conn ref_unblocked_time (ServerCommandTypes.UNSUBSCRIBE_DIAGNOSTIC 0);
607 rpc menv.conn (ref 0.0) (ServerCommandTypes.DISCONNECT)
608 | In_init _ienv ->
609 (* In In_init state, even though we have a 'conn', it's still waiting for *)
610 (* the server to become responsive, so there's no use sending any rpc *)
611 (* messages to the server over it. *)
613 | _ ->
614 (* No other states have a 'conn' to send any disconnect messages over. *)
616 end;
617 Post_shutdown
620 let do_rage (state: state) (ref_unblocked_time: float ref): Rage.result =
621 let open Rage in
622 let items: rageItem list ref = ref [] in
623 let add item = items := item :: !items in
624 let add_data data = add { title = None; data; } in
625 let add_fn fn = if Sys.file_exists fn then add { title = Some fn; data = Sys_utils.cat fn; } in
626 let add_stack (pid, reason) =
627 let pid = string_of_int pid in
628 let stack = try Sys_utils.exec_read_lines ~reverse:true ("pstack " ^ pid)
629 with _ -> begin
630 try Sys_utils.exec_read_lines ~reverse:true ("gstack " ^ pid)
631 with e -> ["unable to pstack - " ^ (Printexc.to_string e)]
632 end in
633 add_data (Printf.sprintf "PSTACK %s (%s) - %s\n\n" pid reason (String.concat "\n" stack))
635 (* logfiles *)
636 begin match get_root_opt () with
637 | Some root -> begin
638 add_fn (ServerFiles.log_link root);
639 add_fn ((ServerFiles.log_link root) ^ ".old");
640 add_fn (ServerFiles.monitor_log_link root);
641 add_fn ((ServerFiles.monitor_log_link root) ^ ".old");
643 let pids = PidLog.get_pids (ServerFiles.pids_file root) in
644 let is_interesting (_, reason) = not (String_utils.string_starts_with reason "slave") in
645 List.filter pids ~f:is_interesting |> List.iter ~f:add_stack
646 with e ->
647 let message = Printexc.to_string e in
648 let stack = Printexc.get_backtrace () in
649 add_data (Printf.sprintf "Failed to get PIDs: %s - %s" message stack)
651 | None -> ()
652 end;
653 (* client *)
654 add_data ("LSP adapter state: " ^ (state_to_string state) ^ "\n");
655 (* client's log of server state *)
656 let tnow = Unix.gettimeofday () in
657 let server_state_to_string (tstate, state) =
658 let open Unix in
659 let tdiff = tnow -. tstate in
660 let state = hh_server_state_to_string state in
661 let tm = Unix.localtime tstate in
662 let ms = int_of_float (tstate *. 1000.) mod 1000 in
663 Printf.sprintf "[%02d:%02d:%02d.%03d] [%03.3fs ago] %s\n"
664 tm.tm_hour tm.tm_min tm.tm_sec ms tdiff state in
665 let server_state_strings = List.map ~f:server_state_to_string !hh_server_state in
666 add_data (String.concat "" ("LSP belief of hh_server_state:\n" :: server_state_strings));
667 (* server *)
668 begin match state with
669 | Main_loop menv -> begin
670 let open Main_env in
671 let items = rpc menv.conn ref_unblocked_time ServerCommandTypes.RAGE in
672 let add i = add { title = i.ServerRageTypes.title; data = i.ServerRageTypes.data; } in
673 List.iter items ~f:add
675 | _ -> ()
676 end;
677 (* that's it! *)
678 !items
680 let do_toggleTypeCoverage
681 (conn : server_conn)
682 (ref_unblocked_time: float ref)
683 (params: ToggleTypeCoverage.params)
684 : unit =
685 (* Currently, the only thing to do on toggling type coverage is turn on dynamic view *)
686 let command = ServerCommandTypes.DYNAMIC_VIEW (params.ToggleTypeCoverage.toggle) in
687 cached_toggle_state := params.ToggleTypeCoverage.toggle;
688 rpc conn ref_unblocked_time command
690 let do_didOpen (conn: server_conn) (ref_unblocked_time: float ref) (params: DidOpen.params)
691 : unit =
692 let open DidOpen in
693 let open TextDocumentItem in
694 let filename = lsp_uri_to_path params.textDocument.uri in
695 let text = params.textDocument.text in
696 let command = ServerCommandTypes.OPEN_FILE (filename, text) in
697 rpc conn ref_unblocked_time command;
700 let do_didClose (conn: server_conn) (ref_unblocked_time: float ref) (params: DidClose.params)
701 : unit =
702 let open DidClose in
703 let open TextDocumentIdentifier in
704 let filename = lsp_uri_to_path params.textDocument.uri in
705 let command = ServerCommandTypes.CLOSE_FILE filename in
706 rpc conn ref_unblocked_time command;
709 let do_didChange
710 (conn: server_conn)
711 (ref_unblocked_time: float ref)
712 (params: DidChange.params)
713 : unit =
714 let open VersionedTextDocumentIdentifier in
715 let open Lsp.DidChange in
716 let lsp_change_to_ide (lsp: DidChange.textDocumentContentChangeEvent)
717 : Ide_api_types.text_edit =
718 { Ide_api_types.
719 range = Option.map lsp.range lsp_range_to_ide;
720 text = lsp.text;
723 let filename = lsp_uri_to_path params.textDocument.uri in
724 let changes = List.map params.contentChanges ~f:lsp_change_to_ide in
725 let command = ServerCommandTypes.EDIT_FILE (filename, changes) in
726 rpc conn ref_unblocked_time command;
729 let do_hover
730 (conn: server_conn)
731 (ref_unblocked_time: float ref)
732 (params: Hover.params)
733 : Hover.result =
734 let (file, line, column) = lsp_file_position_to_hack params in
735 let command =
736 ServerCommandTypes.(INFER_TYPE (FileName file, line, column, false)) in
737 let inferred_type = rpc conn ref_unblocked_time command in
738 match inferred_type with
739 (* Hack server uses None to indicate absence of a result. *)
740 (* We're also catching the non-result "" just in case... *)
741 | None -> None
742 | Some ("", _) -> None
743 | Some (s, _) -> Some { Hover.contents = [MarkedString s]; range = None; }
745 let do_enhanced_hover
746 (conn: server_conn)
747 (ref_unblocked_time: float ref)
748 (params: Hover.params)
749 : Hover.result =
750 let (file, line, column) = lsp_file_position_to_hack params in
751 let command = ServerCommandTypes.IDE_HOVER (ServerCommandTypes.FileName file, line, column) in
752 let infos = rpc conn ref_unblocked_time command in
753 let contents =
754 infos
755 |> List.map ~f:begin fun hoverInfo ->
756 (* Hack server uses None to indicate absence of a result. *)
757 (* We're also catching the non-result "" just in case... *)
758 match hoverInfo with
759 | { HoverService.snippet = ""; _ } -> []
760 | { HoverService.snippet; addendum; _ } ->
761 (MarkedCode ("hack", snippet)) :: (List.map ~f:(fun s -> MarkedString s) addendum)
763 |> List.concat
764 |> List.remove_consecutive_duplicates ~equal:(=)
766 (* We pull the position from the SymbolOccurrence.t record, so I would be
767 surprised if there were any different ones in here. Just take the first
768 non-None one.
769 -wipi *)
770 let range =
771 infos
772 |> List.filter_map ~f:(fun { HoverService.pos; _ } -> pos)
773 |> List.hd
774 |> Option.map ~f:hack_pos_to_lsp_range
776 if contents = [] then None else Some { Hover.contents; range; }
778 let do_definition (conn: server_conn) (ref_unblocked_time: float ref) (params: Definition.params)
779 : Definition.result =
780 let (file, line, column) = lsp_file_position_to_hack params in
781 let command =
782 ServerCommandTypes.(IDENTIFY_FUNCTION (FileName file, line, column)) in
783 let results = rpc conn ref_unblocked_time command in
784 (* What's it like when we return multiple definitions? For instance, if you ask *)
785 (* for the definition of "new C()" then we've now got the definition of the *)
786 (* class "\C" and also of the constructor "\\C::__construct". I think that *)
787 (* users would be happier to only have the definition of the constructor, so *)
788 (* as to jump straight to it without the fuss of clicking to select which one. *)
789 (* That indeed is what Typescript does -- it only gives the constructor. *)
790 (* (VSCode displays multiple definitions with a peek view of them all; *)
791 (* Atom displays them with a small popup showing just file+line of each). *)
792 (* There's one subtlety. If you declare a base class "B" with a constructor, *)
793 (* and a derived class "C" without a constructor, and click on "new C()", then *)
794 (* both Hack and Typescript will take you to the constructor of B. As desired! *)
795 (* Conclusion: given a class+method, we'll return only the method. *)
796 let filtered_results = IdentifySymbolService.filter_redundant results in
797 let rec hack_to_lsp = function
798 | [] -> []
799 | (_occurrence, None) :: l -> hack_to_lsp l
800 | (_occurrence, Some definition) :: l ->
801 (hack_symbol_definition_to_lsp_identifier_location definition ~default_path:file)
802 :: (hack_to_lsp l)
804 hack_to_lsp filtered_results
806 let make_ide_completion_response
807 (result:AutocompleteTypes.ide_result)
808 (filename:string)
809 : Completion.completionList =
810 let open AutocompleteTypes in
811 let open Completion in
812 (* We use snippets to provide parentheses+arguments when autocompleting *)
813 (* method calls e.g. "$c->|" ==> "$c->foo($arg1)". But we'll only do this *)
814 (* there's nothing after the caret: no "$c->|(1)" -> "$c->foo($arg1)(1)" *)
815 let is_caret_followed_by_lparen = result.char_at_pos = '(' in
816 let p = initialize_params_exc () in
818 let hack_to_kind (completion: complete_autocomplete_result)
819 : Completion.completionItemKind option =
820 match completion.res_kind with
821 | Abstract_class_kind
822 | Class_kind -> Some Completion.Class
823 | Method_kind -> Some Completion.Method
824 | Function_kind -> Some Completion.Function
825 | Variable_kind -> Some Completion.Variable
826 | Property_kind -> Some Completion.Property
827 | Class_constant_kind -> Some Completion.Value (* a bit off, but the best we can do *)
828 | Interface_kind
829 | Trait_kind -> Some Completion.Interface
830 | Enum_kind -> Some Completion.Enum
831 | Namespace_kind -> Some Completion.Module
832 | Constructor_kind -> Some Completion.Constructor
833 | Keyword_kind -> Some Completion.Keyword
835 let hack_to_itemType (completion: complete_autocomplete_result) : string option =
836 (* TODO: we're using itemType (left column) for function return types, and *)
837 (* the inlineDetail (right column) for variable/field types. Is that good? *)
838 Option.map completion.func_details ~f:(fun details -> details.return_ty)
840 let hack_to_detail (completion: complete_autocomplete_result) : string =
841 (* TODO: retrieve the actual signature including name+modifiers *)
842 (* For now we just return the type of the completion. In the case *)
843 (* of functions, their function-types have parentheses around them *)
844 (* which we want to strip. In other cases like tuples, no strip. *)
845 match completion.func_details with
846 | None -> completion.res_ty
847 | Some _ -> String_utils.rstrip (String_utils.lstrip completion.res_ty "(") ")"
849 let hack_to_inline_detail (completion: complete_autocomplete_result) : string =
850 match completion.func_details with
851 | None -> hack_to_detail completion
852 | Some details ->
853 (* "(type1 $param1, ...)" *)
854 let f param = Printf.sprintf "%s %s" param.param_ty param.param_name in
855 let params = String.concat ", " (List.map details.params ~f) in
856 Printf.sprintf "(%s)" params
857 (** Returns a tuple of (insertText, insertTextFormat, textEdits). *)
859 let hack_to_insert
860 (completion: complete_autocomplete_result)
861 : [`InsertText of string | `TextEdit of TextEdit.t list] * Completion.insertTextFormat =
862 let use_textedits =
863 let open Initialize in
864 p.initializationOptions.use_textedit_autocomplete
866 match completion.func_details, use_textedits with
867 | Some details, _ when Lsp_helpers.supports_snippets p && not is_caret_followed_by_lparen ->
868 (* "method(${1:arg1}, ...)" but for args we just use param names. *)
869 let f i param = Printf.sprintf "${%i:%s}" (i + 1) param.param_name in
870 let params = String.concat ", " (List.mapi details.params ~f) in
871 (`InsertText (Printf.sprintf "%s(%s)" completion.res_name params), SnippetFormat)
872 | _, false ->
873 (`InsertText completion.res_name, PlainText)
874 | _, true ->
875 (`TextEdit [TextEdit.{
876 range = ide_range_to_lsp (completion.res_replace_pos);
877 newText = completion.res_name;
878 }], PlainText)
880 let hack_completion_to_lsp (completion: complete_autocomplete_result)
881 : Completion.completionItem =
882 let (insertText, insertTextFormat, textEdits) = match hack_to_insert completion with
883 | (`InsertText text, format) -> (Some text, format, [])
884 | (`TextEdit edits, format) -> (None, format, edits)
886 let pos = if Pos.filename completion.res_pos = ""
887 then Pos.set_file filename completion.res_pos
888 else completion.res_pos
890 let data =
891 let (line, start, _) = Pos.info_pos pos in
892 let filename = Pos.filename pos in
893 let base_class = match completion.res_base_class with
894 | Some base_class -> Hh_json.JSON_String base_class
895 | None -> Hh_json.JSON_Null
897 Some (Hh_json.JSON_Object [
898 "filename", Hh_json.JSON_String filename;
899 "line", Hh_json.int_ line;
900 "char", Hh_json.int_ start;
901 "base_class", base_class;
905 label = completion.res_name ^ (if completion.res_kind = Namespace_kind then "\\" else "");
906 kind = hack_to_kind completion;
907 detail = Some (hack_to_detail completion);
908 inlineDetail = Some (hack_to_inline_detail completion);
909 itemType = hack_to_itemType completion;
910 documentation = None; (* This will be filled in by completionItem/resolve. *)
911 sortText = None;
912 filterText = None;
913 insertText;
914 insertTextFormat = Some insertTextFormat;
915 textEdits;
916 command = None;
917 data;
921 isIncomplete = not result.is_complete;
922 items = List.map result.completions ~f:hack_completion_to_lsp;
925 let do_completion_ffp
926 (conn: server_conn)
927 (ref_unblocked_time: float ref)
928 (params: Completion.params)
929 : Completion.result =
930 let open TextDocumentIdentifier in
931 let pos = lsp_position_to_ide params.TextDocumentPositionParams.position in
932 let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument.uri in
933 let command = ServerCommandTypes.IDE_FFP_AUTOCOMPLETE (filename, pos) in
934 let result = rpc conn ref_unblocked_time command in
935 make_ide_completion_response result filename
937 let do_completion_legacy
938 (conn: server_conn)
939 (ref_unblocked_time: float ref)
940 (params: Completion.params)
941 : Completion.result =
942 let open TextDocumentIdentifier in
943 let pos = lsp_position_to_ide params.TextDocumentPositionParams.position in
944 let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument.uri in
945 let delimit_on_namespaces = true in
946 let command = ServerCommandTypes.IDE_AUTOCOMPLETE (filename, pos, delimit_on_namespaces) in
947 let result = rpc conn ref_unblocked_time command in
948 make_ide_completion_response result filename
950 let do_completionItemResolve
951 (conn: server_conn)
952 (ref_unblocked_time: float ref)
953 (params: CompletionItemResolve.params)
954 : CompletionItemResolve.result =
955 match params.Completion.data with
956 | None -> params
957 | Some _ as data ->
958 let filename = Jget.string_exn data "filename" in
959 let line = Jget.int_exn data "line" in
960 let char = Jget.int_exn data "char" in
961 let base_class = Jget.string_opt data "base_class" in
962 let command =
963 ServerCommandTypes.DOCBLOCK_AT (filename, line, char, base_class)
965 let contents = rpc conn ref_unblocked_time command in
966 { params with Completion.documentation = contents }
968 let do_workspaceSymbol
969 (conn: server_conn)
970 (ref_unblocked_time: float ref)
971 (params: WorkspaceSymbol.params)
972 : WorkspaceSymbol.result =
973 let open WorkspaceSymbol in
974 let open SearchUtils in
976 let query = params.query in
977 let query_type = "" in
978 let command = ServerCommandTypes.SEARCH (query, query_type) in
979 let results = rpc conn ref_unblocked_time command in
981 let hack_to_lsp_kind = function
982 | HackSearchService.Class (Some Ast.Cabstract) -> SymbolInformation.Class
983 | HackSearchService.Class (Some Ast.Cnormal) -> SymbolInformation.Class
984 | HackSearchService.Class (Some Ast.Cinterface) -> SymbolInformation.Interface
985 | HackSearchService.Class (Some Ast.Ctrait) -> SymbolInformation.Interface
986 (* LSP doesn't have traits, so we approximate with interface *)
987 | HackSearchService.Class (Some Ast.Cenum) -> SymbolInformation.Enum
988 | HackSearchService.Class (None) -> assert false (* should never happen *)
989 | HackSearchService.Method _ -> SymbolInformation.Method
990 | HackSearchService.ClassVar _ -> SymbolInformation.Property
991 | HackSearchService.Function -> SymbolInformation.Function
992 | HackSearchService.Typedef -> SymbolInformation.Class
993 (* LSP doesn't have typedef, so we approximate with class *)
994 | HackSearchService.Constant -> SymbolInformation.Constant
996 let hack_to_lsp_container = function
997 | HackSearchService.Method (_, scope) -> Some scope
998 | HackSearchService.ClassVar (_, scope) -> Some scope
999 | _ -> None
1001 (* Hack sometimes gives us back items with an empty path, by which it *)
1002 (* intends "whichever path you asked me about". That would be meaningless *)
1003 (* here. If it does, then it'll pick up our default path (also empty), *)
1004 (* which will throw and go into our telemetry. That's the best we can do. *)
1005 let hack_symbol_to_lsp (symbol: HackSearchService.symbol) =
1006 { SymbolInformation.
1007 name = (Utils.strip_ns symbol.name);
1008 kind = hack_to_lsp_kind symbol.result_type;
1009 location = hack_pos_to_lsp_location symbol.pos ~default_path:"";
1010 containerName = hack_to_lsp_container symbol.result_type;
1013 List.map results ~f:hack_symbol_to_lsp
1015 let do_documentSymbol
1016 (conn: server_conn)
1017 (ref_unblocked_time: float ref)
1018 (params: DocumentSymbol.params)
1019 : DocumentSymbol.result =
1020 let open DocumentSymbol in
1021 let open TextDocumentIdentifier in
1022 let open SymbolDefinition in
1024 let filename = lsp_uri_to_path params.textDocument.uri in
1025 let command = ServerCommandTypes.OUTLINE filename in
1026 let results = rpc conn ref_unblocked_time command in
1028 let hack_to_lsp_kind = function
1029 | SymbolDefinition.Function -> SymbolInformation.Function
1030 | SymbolDefinition.Class -> SymbolInformation.Class
1031 | SymbolDefinition.Method -> SymbolInformation.Method
1032 | SymbolDefinition.Property -> SymbolInformation.Property
1033 | SymbolDefinition.Const -> SymbolInformation.Constant
1034 | SymbolDefinition.Enum -> SymbolInformation.Enum
1035 | SymbolDefinition.Interface -> SymbolInformation.Interface
1036 | SymbolDefinition.Trait -> SymbolInformation.Interface
1037 (* LSP doesn't have traits, so we approximate with interface *)
1038 | SymbolDefinition.LocalVar -> SymbolInformation.Variable
1039 | SymbolDefinition.Typeconst -> SymbolInformation.Class
1040 (* e.g. "const type Ta = string;" -- absent from LSP *)
1041 | SymbolDefinition.Typedef -> SymbolInformation.Class
1042 (* e.g. top level type alias -- absent from LSP *)
1043 | SymbolDefinition.Param -> SymbolInformation.Variable
1044 (* We never return a param from a document-symbol-search *)
1046 let hack_symbol_to_lsp definition containerName =
1047 { SymbolInformation.
1048 name = definition.name;
1049 kind = hack_to_lsp_kind definition.kind;
1050 location = hack_symbol_definition_to_lsp_construct_location definition ~default_path:filename;
1051 containerName;
1054 let rec hack_symbol_tree_to_lsp ~accu ~container_name = function
1055 (* Flattens the recursive list of symbols *)
1056 | [] -> List.rev accu
1057 | def :: defs ->
1058 let children = Option.value def.children ~default:[] in
1059 let accu = (hack_symbol_to_lsp def container_name) :: accu in
1060 let accu = hack_symbol_tree_to_lsp accu (Some def.name) children in
1061 hack_symbol_tree_to_lsp accu container_name defs
1063 hack_symbol_tree_to_lsp ~accu:[] ~container_name:None results
1065 let do_findReferences
1066 (conn: server_conn)
1067 (ref_unblocked_time: float ref)
1068 (params: FindReferences.params)
1069 : FindReferences.result =
1070 let open FindReferences in
1072 let {Ide_api_types.line; column;} = lsp_position_to_ide params.position in
1073 let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument in
1074 let include_defs = params.context.includeDeclaration in
1075 let command = ServerCommandTypes.IDE_FIND_REFS
1076 (ServerCommandTypes.FileName filename, line, column, include_defs) in
1077 let results = rpc conn ref_unblocked_time command in
1078 (* TODO: respect params.context.include_declaration *)
1079 match results with
1080 | None -> []
1081 | Some (_name, positions) ->
1082 List.map positions ~f:(hack_pos_to_lsp_location ~default_path:filename)
1085 let do_documentHighlight
1086 (conn: server_conn)
1087 (ref_unblocked_time: float ref)
1088 (params: DocumentHighlight.params)
1089 : DocumentHighlight.result =
1090 let open DocumentHighlight in
1092 let (file, line, column) = lsp_file_position_to_hack params in
1093 let command =
1094 ServerCommandTypes.(IDE_HIGHLIGHT_REFS (FileName file, line, column)) in
1095 let results = rpc conn ref_unblocked_time command in
1097 let hack_range_to_lsp_highlight range =
1099 range = ide_range_to_lsp range;
1100 kind = None;
1103 List.map results ~f:hack_range_to_lsp_highlight
1106 let do_typeCoverage
1107 (conn: server_conn)
1108 (ref_unblocked_time: float ref)
1109 (params: TypeCoverage.params)
1110 : TypeCoverage.result =
1111 let open TypeCoverage in
1113 let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument in
1114 let command = ServerCommandTypes.COVERAGE_LEVELS (ServerCommandTypes.FileName filename) in
1115 let results: Coverage_level.result = rpc conn ref_unblocked_time command in
1116 let results = Coverage_level.merge_adjacent_results results in
1118 (* We want to get a percentage-covered number. We could do that with an *)
1119 (* additional server round trip to ServerCommandTypes.COVERAGE_COUNTS. *)
1120 (* But to avoid that, we'll instead use this rough approximation: *)
1121 (* Count how many checked/unchecked/partial "regions" there are, where *)
1122 (* a "region" is like results_merged, but counting each line separately. *)
1123 let count_region (nchecked, nunchecked, npartial) (pos, level) =
1124 let nlines = (Pos.end_line pos) - (Pos.line pos) + 1 in
1125 match level with
1126 | Ide_api_types.Checked -> (nchecked + nlines, nunchecked, npartial)
1127 | Ide_api_types.Unchecked -> (nchecked, nunchecked + nlines, npartial)
1128 | Ide_api_types.Partial -> (nchecked, nunchecked, npartial + nlines)
1130 let (nchecked, nunchecked, npartial) =
1131 List.fold results ~init:(0,0,0) ~f:count_region in
1133 let ntotal = nchecked + nunchecked + npartial in
1134 let coveredPercent = if ntotal = 0 then 100
1135 else ((nchecked * 100) + (npartial * 100)) / ntotal in
1137 let hack_coverage_to_lsp (pos, level) =
1138 let range = hack_pos_to_lsp_range pos in
1139 match level with
1140 (* We only show diagnostics for completely untypechecked code. *)
1141 | Ide_api_types.Partial
1142 | Ide_api_types.Checked -> None
1143 | Ide_api_types.Unchecked -> Some
1144 { range;
1145 message = "Un-type checked code. Consider adding type annotations.";
1149 coveredPercent;
1150 uncoveredRanges = List.filter_map results ~f:hack_coverage_to_lsp;
1154 let do_formatting_common
1155 (editor_open_files: Lsp.TextDocumentItem.t SMap.t)
1156 (action: ServerFormatTypes.ide_action)
1157 (options: DocumentFormatting.formattingOptions)
1158 : TextEdit.t list =
1159 let open ServerFormatTypes in
1160 let response: ServerFormatTypes.ide_result =
1161 ServerFormat.go_ide editor_open_files action options in
1162 match response with
1163 | Error "File failed to parse without errors" ->
1164 (* If LSP issues a formatting request at a given line+char, but we can't *)
1165 (* calculate a better format for the file due to syntax errors in it, *)
1166 (* then we should return "success and there are no edits to apply" *)
1167 (* rather than "error". *)
1168 (* TODO: let's eliminate hh_format, and incorporate hackfmt into the *)
1169 (* hh_client binary itself, and make make "hackfmt" just a wrapper for *)
1170 (* "hh_client format", and then make it return proper error that we can *)
1171 (* pattern-match upon, rather than hard-coding the string... *)
1173 | Error message ->
1174 raise (Error.InternalError message)
1175 | Ok r ->
1176 let range = ide_range_to_lsp r.range in
1177 let newText = r.new_text in
1178 [{TextEdit.range; newText;}]
1181 let do_documentRangeFormatting
1182 (editor_open_files: Lsp.TextDocumentItem.t SMap.t)
1183 (params: DocumentRangeFormatting.params)
1184 : DocumentRangeFormatting.result =
1185 let open DocumentRangeFormatting in
1186 let open TextDocumentIdentifier in
1187 let action = ServerFormatTypes.Range
1188 { Ide_api_types.
1189 range_filename = lsp_uri_to_path params.textDocument.uri;
1190 file_range = lsp_range_to_ide params.range;
1193 do_formatting_common editor_open_files action params.options
1196 let do_signatureHelp
1197 (conn: server_conn)
1198 (ref_unblocked_time: float ref)
1199 (params: SignatureHelp.params)
1200 : SignatureHelp.result =
1201 let (file, line, column) = lsp_file_position_to_hack params in
1202 let command =
1203 ServerCommandTypes.IDE_SIGNATURE_HELP (ServerCommandTypes.FileName file, line, column)
1205 rpc conn ref_unblocked_time command
1208 let do_documentOnTypeFormatting
1209 (editor_open_files: Lsp.TextDocumentItem.t SMap.t)
1210 (from: string)
1211 (params: DocumentOnTypeFormatting.params)
1212 : DocumentOnTypeFormatting.result =
1213 let open DocumentOnTypeFormatting in
1214 let open TextDocumentIdentifier in
1215 let fixup_position position =
1216 (* temporary workaround for T29372533: Nuclide points at the trigger character... *)
1217 if from = "nuclide" then position
1218 (* ... but other LSP editors such as vscode point one character later *)
1219 else {position with character = position.character - 1}
1221 let action = ServerFormatTypes.Position
1222 { Ide_api_types.
1223 filename = lsp_uri_to_path params.textDocument.uri;
1224 position = lsp_position_to_ide (fixup_position params.position);
1225 } in
1226 do_formatting_common editor_open_files action params.options
1229 let do_documentFormatting
1230 (editor_open_files: Lsp.TextDocumentItem.t SMap.t)
1231 (params: DocumentFormatting.params)
1232 : DocumentFormatting.result =
1233 let open DocumentFormatting in
1234 let open TextDocumentIdentifier in
1235 let action = ServerFormatTypes.Document (lsp_uri_to_path params.textDocument.uri) in
1236 do_formatting_common editor_open_files action params.options
1239 (* do_server_busy: controls the progress / action-required indicator *)
1240 let do_server_busy (state: state) (status: ServerCommandTypes.busy_status) : state =
1241 let open ServerCommandTypes in
1242 let open Main_env in
1243 let p = initialize_params_exc () in
1244 let (progress, action) = match status with
1245 | Needs_local_typecheck -> (Some "Hack: preparing to check edits", None)
1246 | Doing_local_typecheck -> (Some "Hack: checking edits", None)
1247 | Done_local_typecheck -> (None, Some "Hack: save any file to do a whole-program check")
1248 | Doing_global_typecheck true -> (Some "Hack: checking entire project (interruptible)", None)
1249 | Doing_global_typecheck false -> (Some "Hack: checking entire project (blocking)", None)
1250 | Done_global_typecheck -> (None, None)
1252 (* Following code is subtle. Thanks to the magic of the notify_ functions, *)
1253 (* it will either create a new progress/action notification, or update an *)
1254 (* an existing one, or close an existing one, or just no-op, as appropriate *)
1255 match state with
1256 | Main_loop menv ->
1257 Main_loop { menv with
1258 progress = Lsp_helpers.notify_progress p to_stdout menv.progress progress;
1259 actionRequired = Lsp_helpers.notify_actionRequired p to_stdout menv.actionRequired action;
1261 | _ ->
1262 state
1265 (* do_diagnostics: sends notifications for all reported diagnostics; also *)
1266 (* returns an updated "files_with_diagnostics" set of all files for which *)
1267 (* our client currently has non-empty diagnostic reports. *)
1268 let do_diagnostics
1269 (uris_with_diagnostics: SSet.t)
1270 (file_reports: Pos.absolute Errors.error_ list SMap.t)
1271 : SSet.t =
1272 (* Hack sometimes reports a diagnostic on an empty file when it can't *)
1273 (* figure out which file to report. In this case we'll report on the root. *)
1274 (* Nuclide and VSCode both display this fine, though they obviously don't *)
1275 (* let you click-to-go-to-file on it. *)
1276 let default_path = match get_root_opt () with
1277 | None -> failwith "expected root"
1278 | Some root -> Path.to_string root in
1279 let file_reports = match SMap.get "" file_reports with
1280 | None -> file_reports
1281 | Some errors -> SMap.remove "" file_reports |> SMap.add ~combine:(@) default_path errors
1284 let per_file file errors =
1285 hack_errors_to_lsp_diagnostic file errors
1286 |> print_diagnostics
1287 |> Jsonrpc.notify to_stdout "textDocument/publishDiagnostics"
1289 SMap.iter per_file file_reports;
1291 let is_error_free _uri errors = List.is_empty errors in
1292 (* reports_without/reports_with are maps of filename->ErrorList. *)
1293 let (reports_without, reports_with) = SMap.partition is_error_free file_reports in
1294 (* files_without/files_with are sets of filenames *)
1295 let files_without = SMap.bindings reports_without |> List.map ~f:fst in
1296 let files_with = SMap.bindings reports_with |> List.map ~f:fst in
1297 (* uris_without/uris_with are sets of uris *)
1298 let uris_without = List.map files_without ~f:(path_to_lsp_uri ~default_path) |> SSet.of_list in
1299 let uris_with = List.map files_with ~f:(path_to_lsp_uri ~default_path) |> SSet.of_list
1301 (* this is "(uris_with_diagnostics \ uris_without) U uris_with" *)
1302 SSet.union (SSet.diff uris_with_diagnostics uris_without) uris_with
1305 let report_connect_start
1306 (ienv: In_init_env.t)
1307 : state =
1308 let open In_init_env in
1309 assert (not ienv.has_reported_progress);
1310 assert (ienv.dialog = ShowMessageRequest.None);
1311 assert (ienv.progress = Progress.None);
1312 let p = initialize_params_exc () in
1313 (* Our goal behind progress reporting is to let the user know when things *)
1314 (* won't be instantaneous, and to show that things are working as expected. *)
1315 (* Upon connection, if it connects immediately (before we've had 1s idle) *)
1316 (* then nothing will have been displayed. Otherwise, at that first 1s idle, *)
1317 (* which is implemented here, we put up a progress indicator and a dialog *)
1318 (* saying "initializing..."... When it's done, if it took too long, then in *)
1319 (* report_progress_end we put up a "ready" dialog. *)
1321 (* dialog... *)
1322 let handle_result ~result:_ state = match state with
1323 | In_init ienv -> In_init {ienv with In_init_env.dialog = ShowMessageRequest.None}
1324 | _ -> state in
1325 let handle_error ~code:_ ~message:_ ~data:_ state = handle_result "" state in
1326 let dialog = request_showMessage handle_result handle_error
1327 MessageType.InfoMessage "Waiting for hh_server to be ready..." [] in
1329 (* progress indicator... *)
1330 let progress = Lsp_helpers.notify_progress p to_stdout
1331 Progress.None (Some "hh_server initializing") in
1333 In_init { ienv with has_reported_progress = true; dialog; progress; }
1336 let report_connect_progress
1337 (ienv: In_init_env.t)
1338 : state =
1339 let open In_init_env in
1340 assert ienv.has_reported_progress;
1341 let p = initialize_params_exc () in
1342 let tail_env = Option.value_exn ienv.tail_env in
1343 let time = Unix.time () in
1344 let delay_in_secs = int_of_float (time -. ienv.first_start_time) in
1345 (* TODO: better to report time that hh_server has spent initializing *)
1346 let load_state_not_found, tail_msg =
1347 ClientConnect.open_and_get_tail_msg ienv.first_start_time tail_env in
1348 let msg = if load_state_not_found <> ClientConnect.No_failure then
1349 Printf.sprintf
1350 "hh_server initializing (load-state not found - will take a while): %s [%i seconds]"
1351 tail_msg delay_in_secs
1352 else
1353 Printf.sprintf
1354 "hh_server initializing: %s [%i seconds]"
1355 tail_msg delay_in_secs
1357 In_init { ienv with
1358 progress = Lsp_helpers.notify_progress p to_stdout ienv.progress (Some msg);
1362 let report_connect_end
1363 (ienv: In_init_env.t)
1364 : state =
1365 let open In_init_env in
1366 let _state = dismiss_ui (In_init ienv) in
1367 let menv =
1368 { Main_env.
1369 conn = ienv.In_init_env.conn;
1370 needs_idle = true;
1371 editor_open_files = ienv.editor_open_files;
1372 uris_with_diagnostics = SSet.empty;
1373 uris_with_unsaved_changes = ienv.In_init_env.uris_with_unsaved_changes;
1374 dialog = ShowMessageRequest.None;
1375 progress = Progress.None;
1376 actionRequired = ActionRequired.None;
1379 (* alert the user that hack is ready, either by console log or by dialog *)
1380 let time = Unix.time () in
1381 let seconds = int_of_float (time -. ienv.first_start_time) in
1382 let msg = Printf.sprintf "hh_server is now ready, after %i seconds." seconds in
1383 if (time -. ienv.first_start_time > 30.0) then
1384 let handle_result ~result:_ state = match state with
1385 | Main_loop menv -> Main_loop {menv with Main_env.dialog = ShowMessageRequest.None}
1386 | _ -> state in
1387 let handle_error ~code:_ ~message:_ ~data:_ state = handle_result "" state in
1388 let dialog = request_showMessage handle_result handle_error
1389 MessageType.InfoMessage msg [] in
1390 Main_loop {menv with Main_env.dialog;}
1391 else
1392 Main_loop menv
1395 (* After the server has sent 'hello', it means the persistent connection is *)
1396 (* ready, so we can send our backlog of file-edits to the server. *)
1397 let connect_after_hello
1398 (server_conn: server_conn)
1399 (file_edits: Hh_json.json ImmQueue.t)
1400 : unit =
1401 let open Marshal_tools in
1402 let ignore = ref 0.0 in
1403 begin try
1404 let oc = server_conn.oc in
1405 ServerCommand.send_connection_type oc ServerCommandTypes.Persistent;
1406 let fd = Unix.descr_of_out_channel oc in
1407 let response = Marshal_tools.from_fd_with_preamble fd in
1408 if response <> ServerCommandTypes.Connected then
1409 failwith "Didn't get server Connected response";
1410 set_hh_server_state Hh_server_handling_or_ready;
1412 let handle_file_edit (json: Hh_json.json) =
1413 let open Jsonrpc in
1414 let c = Jsonrpc.parse_message ~json ~timestamp:0.0 in
1415 match c.method_ with
1416 | "textDocument/didOpen" -> parse_didOpen c.params |> do_didOpen server_conn ignore
1417 | "textDocument/didChange" -> parse_didChange c.params |> do_didChange server_conn ignore
1418 | "textDocument/didClose" -> parse_didClose c.params |> do_didClose server_conn ignore
1419 | _ -> failwith "should only buffer up didOpen/didChange/didClose"
1421 ImmQueue.iter file_edits ~f:handle_file_edit;
1422 with e ->
1423 let message = Printexc.to_string e in
1424 let stack = Printexc.get_backtrace () in
1425 raise (Server_fatal_connection_exception { message; stack; })
1426 end;
1428 rpc server_conn ignore (ServerCommandTypes.SUBSCRIBE_DIAGNOSTIC 0)
1431 let rec connect_client
1432 (root: Path.t)
1433 ~(autostart: bool)
1434 : server_conn =
1435 let open Exit_status in
1436 (* This basically does the same connection attempt as "hh_client check": *)
1437 (* it makes repeated attempts to connect; it prints useful messages to *)
1438 (* stderr; in case of failure it will raise an exception. Below we're *)
1439 (* catching the main exceptions so we can give a good user-facing error *)
1440 (* text. For other exceptions, they'll end up showing to the user just *)
1441 (* "internal error" with the error code. *)
1442 let env_connect =
1443 { ClientConnect.
1444 root;
1445 autostart;
1446 force_dormant_start = false;
1447 retries = Some 3; (* each retry takes up to 1 second *)
1448 expiry = None; (* we can limit retries by time as well as by count *)
1449 no_load = false; (* only relevant when autostart=true *)
1450 profile_log = false; (* irrelevant *)
1451 ai_mode = None; (* only relevant when autostart=true *)
1452 progress_callback = ClientConnect.null_progress_reporter; (* we're fast! *)
1453 do_post_handoff_handshake = false;
1454 ignore_hh_version = false;
1455 use_priority_pipe = true;
1456 } in
1458 let ClientConnect.{channels = ic, oc; _} =
1459 ClientConnect.connect env_connect in
1460 can_autostart_after_mismatch := false;
1461 let pending_messages = Queue.create () in
1462 { ic; oc; pending_messages; }
1463 with
1464 | Exit_with Build_id_mismatch when !can_autostart_after_mismatch ->
1465 (* Raised when the server was running an old version. We'll retry once. *)
1466 can_autostart_after_mismatch := false;
1467 connect_client root ~autostart:true
1470 let do_initialize () : Initialize.result =
1471 let open Initialize in
1473 server_capabilities = {
1474 textDocumentSync = {
1475 want_openClose = true;
1476 want_change = IncrementalSync;
1477 want_willSave = false;
1478 want_willSaveWaitUntil = false;
1479 want_didSave = Some { includeText = false }
1481 hoverProvider = true;
1482 completionProvider = Some {
1483 resolveProvider = true;
1484 completion_triggerCharacters = ["$"; ">"; "\\"; ":"; "<"];
1486 signatureHelpProvider = Some { sighelp_triggerCharacters = ["("; ","] };
1487 definitionProvider = true;
1488 referencesProvider = true;
1489 documentHighlightProvider = true;
1490 documentSymbolProvider = true;
1491 workspaceSymbolProvider = true;
1492 codeActionProvider = false;
1493 codeLensProvider = None;
1494 documentFormattingProvider = true;
1495 documentRangeFormattingProvider = true;
1496 documentOnTypeFormattingProvider = Some {
1497 firstTriggerCharacter = ";";
1498 moreTriggerCharacter = ["}"];
1500 renameProvider = false;
1501 documentLinkProvider = None;
1502 executeCommandProvider = None;
1503 typeCoverageProvider = true;
1504 rageProvider = true;
1509 let start_server (root: Path.t) : unit =
1510 (* This basically does "hh_client start": a single attempt to open the *)
1511 (* socket, send+read version and compare for mismatch, send handoff and *)
1512 (* read response. It will print information to stderr. If the server is in *)
1513 (* an unresponsive or invalid state then it will kill the server. Next if *)
1514 (* necessary it tries to spawn the server and wait until the monitor is *)
1515 (* responsive enough to print "ready". It will do a hard program exit if *)
1516 (* there were spawn problems. *)
1517 let env_start =
1518 { ClientStart.
1519 root;
1520 no_load = false;
1521 profile_log = false;
1522 ai_mode = None;
1523 silent = true;
1524 exit_on_failure = false;
1525 debug_port = None;
1526 ignore_hh_version = false;
1527 dynamic_view = !cached_toggle_state;
1528 } in
1529 let _exit_status = ClientStart.main env_start in
1533 (* connect: this method either connects to the monitor and leaves in an *)
1534 (* In_init state waiting for the server hello, or it fails to connect and *)
1535 (* leaves in a Lost_server state. You might call this from Pre_init or *)
1536 (* Lost_server states, obviously. But you can also call it from In_init state *)
1537 (* if you want to give up on the prior attempt at connection and try again. *)
1538 let rec connect (state: state) : state =
1539 let root = match get_root_opt () with
1540 | Some root -> root
1541 | None -> assert false in
1542 begin match state with
1543 | In_init { In_init_env.conn; _ } -> begin try
1544 Timeout.shutdown_connection conn.ic;
1545 Timeout.close_in_noerr conn.ic
1546 with _ -> ()
1548 | Pre_init | Lost_server _ -> ()
1549 | _ -> failwith "connect only in Pre_init, In_init or Lost_server state"
1550 end;
1552 let conn = connect_client root ~autostart:false in
1553 set_hh_server_state Hh_server_initializing;
1554 match state with
1555 | In_init ienv ->
1556 In_init { ienv with In_init_env.conn; most_recent_start_time = Unix.time(); }
1557 | _ ->
1558 let state = dismiss_ui state in
1559 In_init { In_init_env.
1560 conn;
1561 first_start_time = Unix.time();
1562 most_recent_start_time = Unix.time();
1563 editor_open_files =
1564 Option.value (get_editor_open_files state) ~default:SMap.empty;
1565 (* uris_with_unsaved_changes should always be empty here: *)
1566 (* Pre_init will of course be empty; *)
1567 (* Lost_server will exit rather than reconnect with unsaved changes. *)
1568 uris_with_unsaved_changes = get_uris_with_unsaved_changes state;
1569 (* Similarly, file_edits will be empty: *)
1570 file_edits = ImmQueue.empty;
1571 tail_env = Some (Tail.create_env (ServerFiles.log_link root));
1572 has_reported_progress = false;
1573 dialog = ShowMessageRequest.None;
1574 progress = Progress.None;
1576 with e ->
1577 (* Exit_with Out_of_retries, Exit_with Out_of_time: raised when we *)
1578 (* couldn't complete the handshake up to handoff within 3 attempts over *)
1579 (* 3 seconds. Maybe the informant is stopping anything from happening *)
1580 (* until a rebase has settled? *)
1581 (* Exit_with No_server_running: raised when (1) the server's simply not *)
1582 (* running, or there's some other reason why the connection was refused *)
1583 (* or timed-out and no lockfile is present; (2) the server was dormant *)
1584 (* and had already received too many pending connection requests. *)
1585 (* Exit_with Monitor_connection_failure: raised when the lockfile is *)
1586 (* present but connection-attempt to the monitor times out - maybe it's *)
1587 (* under DDOS, or maybe it's declining to answer new connections. *)
1588 let stack = Printexc.get_backtrace () in
1589 let (code, message, _data) = Lsp_fmt.get_error_info e in
1590 let longMessage = Printf.sprintf "connect failed: %s [%i]\n%s" message code stack in
1591 let () = Lsp_helpers.telemetry_error to_stdout longMessage in
1592 let open Exit_status in
1593 let new_hh_server_state = match e with
1594 | Exit_with Build_id_mismatch
1595 | Exit_with No_server_running -> Hh_server_stopped
1596 | Exit_with Out_of_retries
1597 | Exit_with Out_of_time -> Hh_server_denying_connection
1598 | _ -> Hh_server_unknown
1600 let explanation = match e with
1601 | Exit_with Out_of_retries
1602 | Exit_with Out_of_time ->
1603 Lost_env.Wait_required "hh_server is waiting for things to settle"
1604 | _ ->
1605 Lost_env.Action_required ("hh_server: " ^ message)
1607 do_lost_server state ~allow_immediate_reconnect:false
1608 { Lost_env.
1609 explanation;
1610 new_hh_server_state;
1611 start_on_click = true;
1612 trigger_on_lock_file = true;
1613 trigger_on_lsp = false;
1617 and reconnect_from_lost_if_necessary
1618 (state: state)
1619 (reason: [> `Event of event | `Force_regain ])
1620 : state =
1621 let open Lost_env in
1622 let should_reconnect = match state, reason with
1623 | Lost_server _, `Force_regain -> true
1624 | Lost_server lenv, `Event Client_message c
1625 when lenv.p.trigger_on_lsp && c.Jsonrpc.kind <> Jsonrpc.Response -> true
1626 | Lost_server lenv, `Event Tick when lenv.p.trigger_on_lock_file ->
1627 MonitorConnection.server_exists lenv.lock_file
1628 | _, _ -> false
1630 if should_reconnect then
1631 let has_unsaved_changes = not (SSet.is_empty (get_uris_with_unsaved_changes state)) in
1632 let current_version = read_hhconfig_version () in
1633 let needs_to_terminate = has_unsaved_changes || !hhconfig_version <> current_version in
1634 if needs_to_terminate then
1635 (* In these cases we have to terminate our LSP server, and trust the *)
1636 (* client to restart us. Note that we can't do clientStart because that *)
1637 (* would start our (old) version of hh_server, not the new one! *)
1638 let unsaved = get_uris_with_unsaved_changes state |> SSet.elements in
1639 let unsaved_str = if unsaved = [] then "[None]" else String.concat "\n" unsaved in
1640 let message = "Unsaved files:\n" ^ unsaved_str ^
1641 "\nVersion in hhconfig that spawned the current hh_client: " ^ !hhconfig_version ^
1642 "\nVersion in hhconfig currently: " ^ current_version ^
1643 "\n" in
1644 Lsp_helpers.telemetry_log to_stdout message;
1645 exit_fail ()
1646 else
1647 connect state
1648 else
1649 state
1652 (* do_lost_server: handles the various ways we might lose hh_server. We keep *)
1653 (* the LSP server alive, and will (elsewhere) listen for the various triggers *)
1654 (* of getting the server back. *)
1655 and do_lost_server (state: state) ?(allow_immediate_reconnect = true) (p: Lost_env.params) : state =
1656 let open Lost_env in
1657 set_hh_server_state p.new_hh_server_state;
1658 let initialize_params = initialize_params_exc () in
1660 let no_op = match p.explanation, state with
1661 | Wait_required _, Lost_server { progress; _ }
1662 when progress <> Progress.None -> true
1663 | Action_required _, Lost_server { actionRequired; _ }
1664 when actionRequired <> ActionRequired.None -> true
1665 | _ -> false in
1666 (* If we already display a progress indicator, and call do_lost_server *)
1667 (* to display a progress indicator, then we won't do anything. Likewise, *)
1668 (* if we already display an error dialog with ANY TEXT and Restart button, *)
1669 (* and call do_lost_server to display any other text in the error dialog, *)
1670 (* it makes for a nicer UI to simply leave the old text up. *)
1671 if no_op then state else
1673 let state = dismiss_ui state in
1674 let uris_with_unsaved_changes = get_uris_with_unsaved_changes state in
1675 let editor_open_files =
1676 Option.value (get_editor_open_files state) ~default:SMap.empty in
1678 let lock_file = match get_root_opt () with
1679 | None -> assert false
1680 | Some root -> ServerFiles.lock_file root
1682 let reconnect_immediately = allow_immediate_reconnect &&
1683 p.trigger_on_lock_file && MonitorConnection.server_exists lock_file
1686 (* These helper functions are for the dialog *)
1687 let dialog_ref : ShowMessageRequest.t ref = ref ShowMessageRequest.None in
1688 let clear_dialog_flag (state: state) : state =
1689 match state with
1690 | Lost_server lenv ->
1691 (* TODO(ljw): The following != test is "implementation-specific". *)
1692 (* Goal is so that if we had one dialog up, then dismiss_ui it which *)
1693 (* sends $/cancelRequest, then put up another dialog, then the editor *)
1694 (* sends back a RequestCancelled error in response to the first dialog,*)
1695 (* we don't want to clear the flag that's now there for the second *)
1696 (* dialog. This != test achieves that. But ocaml only guarantees *)
1697 (* behavior of != on mutable things, which ours is not... *)
1698 if lenv.dialog != !dialog_ref then Lost_server lenv
1699 else Lost_server { lenv with dialog = ShowMessageRequest.None; }
1700 | _ -> state
1702 let handle_error ~code:_ ~message:_ ~data:_ state =
1703 state |> clear_dialog_flag
1705 let handle_result ~result state =
1706 let state = state |> clear_dialog_flag in
1707 let result = Jget.string_d result "title" ~default:"" in
1708 match result, state with
1709 | "Restart", Lost_server _ ->
1710 if p.start_on_click then begin
1711 let root = match get_root_opt () with
1712 | None -> failwith "we should have root by now"
1713 | Some root -> root
1715 start_server root
1716 end;
1717 reconnect_from_lost_if_necessary state `Force_regain
1718 | _ -> state
1721 if reconnect_immediately then
1722 let lost_state = Lost_server { Lost_env.
1724 editor_open_files;
1725 uris_with_unsaved_changes;
1726 lock_file;
1727 dialog = ShowMessageRequest.None;
1728 actionRequired = ActionRequired.None;
1729 progress = Progress.None;
1730 } in
1731 Lsp_helpers.telemetry_log to_stdout "Reconnecting immediately to hh_server";
1732 let new_state = reconnect_from_lost_if_necessary lost_state `Force_regain in
1733 new_state
1734 else
1735 let progress, actionRequired, dialog = match p.explanation with
1736 | Wait_required msg ->
1737 let progress = Lsp_helpers.notify_progress initialize_params to_stdout
1738 Progress.None (Some msg) in
1739 progress, ActionRequired.None, ShowMessageRequest.None
1740 | Action_required msg ->
1741 let actionRequired = Lsp_helpers.notify_actionRequired initialize_params to_stdout
1742 ActionRequired.None (Some msg) in
1743 let dialog = request_showMessage handle_result handle_error
1744 MessageType.ErrorMessage msg ["Restart"] in
1745 Progress.None, actionRequired, dialog
1747 dialog_ref := dialog;
1748 Lost_server { Lost_env.
1750 editor_open_files;
1751 uris_with_unsaved_changes;
1752 lock_file;
1753 dialog;
1754 actionRequired;
1755 progress;
1760 let dismiss_ready_dialog_if_necessary (state: state) (event: event) : state =
1761 (* We'll auto-dismiss the ready dialog if it was up, in response to user *)
1762 (* actions like typing or hover, and in response to a lost server. *)
1763 let open Jsonrpc in
1764 let open Main_env in
1765 match state with
1766 | Main_loop menv -> begin
1767 match event with
1768 | Client_message {kind = Jsonrpc.Response; _} ->
1769 state
1770 | Client_message _
1771 | Server_message {push=ServerCommandTypes.NEW_CLIENT_CONNECTED; _} ->
1772 let dialog = dismiss_showMessageRequest menv.dialog in
1773 Main_loop { menv with dialog; }
1774 | _ ->
1775 state
1777 | _ -> state
1780 let handle_idle_if_necessary (state: state) (event: event) : state =
1781 match state with
1782 | Main_loop menv when event <> Tick -> Main_loop { menv with Main_env.needs_idle = true; }
1783 | _ -> state
1785 let track_open_files (state: state) (event: event) : state =
1786 let open Jsonrpc in
1787 (* We'll keep track of which files are opened by the editor. *)
1788 let prev_opened_files =
1789 Option.value (get_editor_open_files state) ~default:SMap.empty in
1790 let editor_open_files = match event with
1791 | Client_message c when c.method_ = "textDocument/didOpen" ->
1792 let params = parse_didOpen c.params in
1793 let doc = params.DidOpen.textDocument in
1794 let uri = params.DidOpen.textDocument.TextDocumentItem.uri in
1795 SMap.add uri doc prev_opened_files
1796 | Client_message c when c.method_ = "textDocument/didChange" ->
1797 let params = parse_didChange c.params in
1798 let uri = params.DidChange.textDocument.VersionedTextDocumentIdentifier.uri in
1799 let doc = SMap.get uri prev_opened_files in
1800 begin
1801 let open Lsp.TextDocumentItem in
1802 match doc with
1803 | Some doc ->
1804 let doc' = { doc with
1805 version = params.DidChange.textDocument.VersionedTextDocumentIdentifier.version;
1806 text = Lsp_helpers.apply_changes_unsafe doc.text params.DidChange.contentChanges;
1807 } in
1808 SMap.add uri doc' prev_opened_files
1809 | None -> prev_opened_files
1811 | Client_message c when c.method_ = "textDocument/didClose" ->
1812 let params = parse_didClose c.params in
1813 let uri = params.DidClose.textDocument.TextDocumentIdentifier.uri in
1814 SMap.remove uri prev_opened_files
1815 | _ ->
1816 prev_opened_files
1818 match state with
1819 | Main_loop menv -> Main_loop { menv with Main_env.editor_open_files; }
1820 | In_init ienv -> In_init { ienv with In_init_env.editor_open_files; }
1821 | Lost_server lenv -> Lost_server { lenv with Lost_env.editor_open_files; }
1822 | _ -> state
1825 let track_edits_if_necessary (state: state) (event: event) : state =
1826 let open Jsonrpc in
1827 (* We'll keep track of which files have unsaved edits. Note that not all *)
1828 (* clients send didSave messages; for those we only rely on didClose. *)
1829 let previous = get_uris_with_unsaved_changes state in
1830 let uris_with_unsaved_changes = match event with
1831 | Client_message ({ method_ = "textDocument/didChange"; _ } as c) ->
1832 let params = parse_didChange c.params in
1833 let uri = params.DidChange.textDocument.VersionedTextDocumentIdentifier.uri in
1834 SSet.add uri previous
1835 | Client_message ({ method_ = "textDocument/didClose"; _ } as c) ->
1836 let params = parse_didClose c.params in
1837 let uri = params.DidClose.textDocument.TextDocumentIdentifier.uri in
1838 SSet.remove uri previous
1839 | Client_message ({ method_ = "textDocument/didSave"; _ } as c) ->
1840 let params = parse_didSave c.params in
1841 let uri = params.DidSave.textDocument.TextDocumentIdentifier.uri in
1842 SSet.remove uri previous
1843 | _ ->
1844 previous
1846 match state with
1847 | Main_loop menv -> Main_loop { menv with Main_env.uris_with_unsaved_changes; }
1848 | In_init ienv -> In_init { ienv with In_init_env.uris_with_unsaved_changes; }
1849 | Lost_server lenv -> Lost_server { lenv with Lost_env.uris_with_unsaved_changes; }
1850 | _ -> state
1853 let log_response_if_necessary
1854 (event: event)
1855 (response: Hh_json.json option)
1856 (unblocked_time: float)
1857 : unit =
1858 let open Jsonrpc in
1859 match event with
1860 | Client_message c ->
1861 let json = c.json |> Hh_json.json_truncate |> Hh_json.json_to_string in
1862 let json_response = match response with
1863 | None -> ""
1864 | Some json -> json |> Hh_json.json_truncate |> Hh_json.json_to_string
1866 HackEventLogger.client_lsp_method_handled
1867 ~root:(get_root_opt ())
1868 ~method_:(if c.kind = Response then "[response]" else c.method_)
1869 ~kind:(kind_to_string c.kind)
1870 ~start_queue_time:c.timestamp
1871 ~start_hh_server_state:(get_older_hh_server_state c.timestamp |> hh_server_state_to_string)
1872 ~start_handle_time:unblocked_time
1873 ~json
1874 ~json_response
1875 | _ -> ()
1878 let hack_log_error
1879 (event: event option)
1880 (message: string)
1881 (stack: string)
1882 (source: string)
1883 (unblocked_time: float)
1884 : unit =
1885 let root = get_root_opt () in
1886 match event with
1887 | Some Client_message c ->
1888 let open Jsonrpc in
1889 let json = c.json |> Hh_json.json_truncate |> Hh_json.json_to_string in
1890 HackEventLogger.client_lsp_method_exception
1891 ~root
1892 ~method_:c.method_
1893 ~kind:(kind_to_string c.kind)
1894 ~start_queue_time:c.timestamp
1895 ~start_hh_server_state:(get_older_hh_server_state c.timestamp |> hh_server_state_to_string)
1896 ~start_handle_time:unblocked_time
1897 ~json
1898 ~message
1899 ~stack
1900 ~source
1901 | _ ->
1902 HackEventLogger.client_lsp_exception
1903 ~root
1904 ~message
1905 ~stack
1906 ~source
1909 (* cancel_if_stale: If a message is stale, throw the necessary exception to
1910 cancel it. A message is considered stale if it's sufficiently old and there
1911 are other messages in the queue that are newer than it. *)
1912 let short_timeout = 2.5
1913 let long_timeout = 15.0
1915 let cancel_if_stale (client: Jsonrpc.queue) (message: Jsonrpc.message) (timeout: float) : unit =
1916 let message_received_time = message.Jsonrpc.timestamp in
1917 let time_elapsed = (Unix.gettimeofday ()) -. message_received_time in
1918 if time_elapsed >= timeout && Jsonrpc.has_message client
1919 then raise (Error.RequestCancelled "request timed out")
1922 (************************************************************************)
1923 (** Message handling **)
1924 (************************************************************************)
1926 (* handle_event: Process and respond to a message, and update the LSP state
1927 machine accordingly. In case the message was a request, it returns the
1928 json it responded with, so the caller can log it. *)
1929 let handle_event
1930 ~(env: env)
1931 ~(state: state ref)
1932 ~(client: Jsonrpc.queue)
1933 ~(event: event)
1934 ~(ref_unblocked_time: float ref)
1935 : unit =
1936 let open Jsonrpc in
1937 let open Main_env in
1938 match !state, event with
1939 (* response *)
1940 | _, Client_message c when c.kind = Jsonrpc.Response ->
1941 let id = match c.id with
1942 | Some (Hh_json.JSON_Number id) -> NumberId (int_of_string id)
1943 | Some (Hh_json.JSON_String id) -> StringId id
1944 | _ -> failwith "malformed response id" in
1945 let on_result, on_error = match IdMap.get id !callbacks_outstanding with
1946 | Some callbacks -> callbacks
1947 | None -> failwith "response id doesn't correspond to an outstanding request" in
1948 if Option.is_some c.error then
1949 let code = Jget.int_exn c.error "code" in
1950 let message = Jget.string_exn c.error "message" in
1951 let data = Jget.val_opt c.error "data" in
1952 state := on_error code message data !state
1953 else
1954 state := on_result c.result !state
1956 (* shutdown request *)
1957 | _, Client_message c when c.method_ = "shutdown" ->
1958 state := do_shutdown !state ref_unblocked_time;
1959 print_shutdown () |> Jsonrpc.respond to_stdout c;
1961 (* cancel notification *)
1962 | _, Client_message c when c.method_ = "$/cancelRequest" ->
1963 (* For now, we'll ignore it. *)
1966 (* exit notification *)
1967 | _, Client_message c when c.method_ = "exit" ->
1968 if !state = Post_shutdown then exit_ok () else exit_fail ()
1970 (* rage request *)
1971 | _, Client_message c when c.method_ = "telemetry/rage" ->
1972 do_rage !state ref_unblocked_time |> print_rage |> Jsonrpc.respond to_stdout c
1974 (* initialize request *)
1975 | Pre_init, Client_message c when c.method_ = "initialize" ->
1976 let initialize_params = c.params |> parse_initialize in
1977 initialize_params_ref := Some initialize_params;
1978 hhconfig_version := read_hhconfig_version ();
1979 state := connect !state;
1980 do_initialize () |> print_initialize |> Jsonrpc.respond to_stdout c;
1981 if not @@ Sys_utils.is_test_mode () then
1982 Lsp_helpers.telemetry_log to_stdout ("Version in hhconfig=" ^ !hhconfig_version)
1984 (* any request/notification if we haven't yet initialized *)
1985 | Pre_init, Client_message _c ->
1986 raise (Error.ServerNotInitialized "Server not yet initialized")
1988 (* any request/notification if we're not yet ready *)
1989 | In_init ienv, Client_message c ->
1990 let open In_init_env in
1991 begin match c.method_ with
1992 | "textDocument/didOpen"
1993 | "textDocument/didChange"
1994 | "textDocument/didClose" ->
1995 (* These three crucial-for-correctness notifications will be buffered *)
1996 (* up so we'll be able to handle them when we're ready. *)
1997 state := In_init { ienv with file_edits = ImmQueue.push ienv.file_edits c.json }
1998 | _ ->
1999 raise (Error.RequestCancelled (Hh_server_initializing |> hh_server_state_to_string))
2000 (* We deny all other requests. Operation_cancelled is the only *)
2001 (* error-response that won't produce logs/warnings on most clients. *)
2004 (* idle tick while waiting for server to complete initialization *)
2005 | In_init ienv, Tick ->
2006 let open In_init_env in
2007 let time = Unix.time () in
2008 let delay_in_secs = int_of_float (time -. ienv.most_recent_start_time) in
2009 if not ienv.has_reported_progress then
2010 state := report_connect_start ienv
2011 else if delay_in_secs <= 10 then
2012 state := report_connect_progress ienv
2013 else begin
2014 state := connect !state (* terminate + retry the connection *)
2017 (* server completes initialization *)
2018 | In_init ienv, Server_hello ->
2019 connect_after_hello ienv.In_init_env.conn ienv.In_init_env.file_edits;
2020 state := report_connect_end ienv
2022 (* any "hello" from the server when we weren't expecting it. This is so *)
2023 (* egregious that we can't trust anything more from the server. *)
2024 | _, Server_hello ->
2025 let message = "Unexpected hello" in
2026 let stack = "" in
2027 raise (Server_fatal_connection_exception { Marshal_tools.message; stack; })
2029 (* Tick when we're connected to the server and have empty queue *)
2030 | Main_loop menv, Tick when menv.needs_idle ->
2031 (* If we're connected to a server and have no more messages in the queue, *)
2032 (* then we must let the server know we're idle, so it will be free to *)
2033 (* handle command-line requests. *)
2034 state := Main_loop { menv with needs_idle = false; };
2035 rpc menv.conn ref_unblocked_time ServerCommandTypes.IDE_IDLE
2037 (* textDocument/hover request *)
2038 | Main_loop menv, Client_message c when c.method_ = "textDocument/hover" ->
2039 cancel_if_stale client c short_timeout;
2040 let hover_command = if env.use_enhanced_hover then do_enhanced_hover else do_hover in
2041 parse_hover c.params |> hover_command menv.conn ref_unblocked_time
2042 |> print_hover |> Jsonrpc.respond to_stdout c
2044 (* textDocument/definition request *)
2045 | Main_loop menv, Client_message c when c.method_ = "textDocument/definition" ->
2046 cancel_if_stale client c short_timeout;
2047 parse_definition c.params |> do_definition menv.conn ref_unblocked_time
2048 |> print_definition |> Jsonrpc.respond to_stdout c
2050 (* textDocument/completion request *)
2051 | Main_loop menv, Client_message c when c.method_ = "textDocument/completion" ->
2052 let do_completion =
2053 if env.use_ffp_autocomplete then do_completion_ffp else do_completion_legacy in
2054 cancel_if_stale client c short_timeout;
2055 parse_completion c.params |> do_completion menv.conn ref_unblocked_time
2056 |> print_completion |> Jsonrpc.respond to_stdout c
2058 (* completionItem/resolve request *)
2059 | Main_loop menv, Client_message c when c.method_ = "completionItem/resolve" ->
2060 cancel_if_stale client c short_timeout;
2061 parse_completionItem c.params
2062 |> do_completionItemResolve menv.conn ref_unblocked_time
2063 |> print_completionItem
2064 |> Jsonrpc.respond to_stdout c
2066 (* workspace/symbol request *)
2067 | Main_loop menv, Client_message c when c.method_ = "workspace/symbol" ->
2068 parse_workspaceSymbol c.params |> do_workspaceSymbol menv.conn ref_unblocked_time
2069 |> print_workspaceSymbol |> Jsonrpc.respond to_stdout c
2071 (* textDocument/documentSymbol request *)
2072 | Main_loop menv, Client_message c when c.method_ = "textDocument/documentSymbol" ->
2073 parse_documentSymbol c.params |> do_documentSymbol menv.conn ref_unblocked_time
2074 |> print_documentSymbol |> Jsonrpc.respond to_stdout c
2076 (* textDocument/references request *)
2077 | Main_loop menv, Client_message c when c.method_ = "textDocument/references" ->
2078 cancel_if_stale client c long_timeout;
2079 parse_findReferences c.params |> do_findReferences menv.conn ref_unblocked_time
2080 |> print_findReferences |> Jsonrpc.respond to_stdout c
2082 (* textDocument/documentHighlight *)
2083 | Main_loop menv, Client_message c when c.method_ = "textDocument/documentHighlight" ->
2084 cancel_if_stale client c short_timeout;
2085 parse_documentHighlight c.params |> do_documentHighlight menv.conn ref_unblocked_time
2086 |> print_documentHighlight |> Jsonrpc.respond to_stdout c
2088 (* textDocument/typeCoverage *)
2089 | Main_loop menv, Client_message c when c.method_ = "textDocument/typeCoverage" ->
2090 parse_typeCoverage c.params |> do_typeCoverage menv.conn ref_unblocked_time
2091 |> print_typeCoverage |> Jsonrpc.respond to_stdout c
2093 (* textDocument/formatting *)
2094 | Main_loop menv, Client_message c when c.method_ = "textDocument/formatting" ->
2095 parse_documentFormatting c.params
2096 |> do_documentFormatting menv.editor_open_files
2097 |> print_documentFormatting |> Jsonrpc.respond to_stdout c
2099 (* textDocument/formatting *)
2100 | Main_loop menv, Client_message c
2101 when c.method_ = "textDocument/rangeFormatting" ->
2102 parse_documentRangeFormatting c.params
2103 |> do_documentRangeFormatting menv.editor_open_files
2104 |> print_documentRangeFormatting |> Jsonrpc.respond to_stdout c
2106 (* textDocument/onTypeFormatting *)
2107 | Main_loop menv, Client_message c when c.method_ = "textDocument/onTypeFormatting" ->
2108 cancel_if_stale client c short_timeout;
2109 parse_documentOnTypeFormatting c.params
2110 |> do_documentOnTypeFormatting menv.editor_open_files env.from
2111 |> print_documentOnTypeFormatting |> Jsonrpc.respond to_stdout c
2113 (* textDocument/didOpen notification *)
2114 | Main_loop menv, Client_message c when c.method_ = "textDocument/didOpen" ->
2115 parse_didOpen c.params |> do_didOpen menv.conn ref_unblocked_time
2117 | Main_loop menv, Client_message c when c.method_ = "workspace/toggleTypeCoverage" ->
2118 parse_toggleTypeCoverage c.params |> do_toggleTypeCoverage menv.conn ref_unblocked_time
2120 (* textDocument/didClose notification *)
2121 | Main_loop menv, Client_message c when c.method_ = "textDocument/didClose" ->
2122 parse_didClose c.params |> do_didClose menv.conn ref_unblocked_time
2124 (* textDocument/didChange notification *)
2125 | Main_loop menv, Client_message c when c.method_ = "textDocument/didChange" ->
2126 parse_didChange c.params |> do_didChange menv.conn ref_unblocked_time
2128 (* textDocument/didSave notification *)
2129 | Main_loop _menv, Client_message c when c.method_ = "textDocument/didSave" ->
2132 (* textDocument/signatureHelp notification *)
2133 | Main_loop menv, Client_message c when c.method_ = "textDocument/signatureHelp" ->
2134 parse_textDocumentPositionParams c.params
2135 |> do_signatureHelp menv.conn ref_unblocked_time
2136 |> print_signatureHelp
2137 |> Jsonrpc.respond to_stdout c
2139 (* server busy status *)
2140 | _, Server_message {push=ServerCommandTypes.BUSY_STATUS status; _} ->
2141 state := do_server_busy !state status
2143 (* textDocument/publishDiagnostics notification *)
2144 | Main_loop menv, Server_message {push=ServerCommandTypes.DIAGNOSTIC (_, errors); _} ->
2145 let uris_with_diagnostics = do_diagnostics menv.uris_with_diagnostics errors in
2146 state := Main_loop { menv with uris_with_diagnostics; }
2148 (* any server diagnostics that come after we've shut down *)
2149 | _, Server_message {push=ServerCommandTypes.DIAGNOSTIC _; _} ->
2152 (* catch-all for client reqs/notifications we haven't yet implemented *)
2153 | Main_loop _menv, Client_message c ->
2154 let message = Printf.sprintf "not implemented: %s" c.method_ in
2155 raise (Error.MethodNotFound message)
2157 (* catch-all for requests/notifications after shutdown request *)
2158 | Post_shutdown, Client_message _c ->
2159 raise (Error.InvalidRequest "already received shutdown request")
2161 (* server shut-down request *)
2162 | Main_loop _menv, Server_message {push=ServerCommandTypes.NEW_CLIENT_CONNECTED; _} ->
2163 state := dismiss_ready_dialog_if_necessary !state event;
2164 state := do_lost_server !state { Lost_env.
2165 explanation = Lost_env.Action_required "hh_server is active in another window.";
2166 new_hh_server_state = Hh_server_stolen;
2167 start_on_click = false;
2168 trigger_on_lock_file = false;
2169 trigger_on_lsp = true;
2172 (* server shut-down request, unexpected *)
2173 | _, Server_message {push=ServerCommandTypes.NEW_CLIENT_CONNECTED; _} ->
2174 let open Marshal_tools in
2175 let message = "unexpected close of absent server" in
2176 let stack = "" in
2177 raise (Server_fatal_connection_exception { message; stack; })
2179 (* server fatal shutdown *)
2180 | _, Server_message {push=ServerCommandTypes.FATAL_EXCEPTION e; _} ->
2181 raise (Server_fatal_connection_exception e)
2183 (* server non-fatal exception *)
2184 | _, Server_message {push=ServerCommandTypes.NONFATAL_EXCEPTION e; _} ->
2185 raise (Server_nonfatal_exception e)
2187 (* idle tick. No-op. *)
2188 | _, Tick ->
2189 EventLogger.flush ()
2191 (* client message when we've lost the server *)
2192 | Lost_server lenv, Client_message _c ->
2193 let open Lost_env in
2194 (* if trigger_on_lsp_method is set, our caller should already have *)
2195 (* transitioned away from this state. *)
2196 assert (not lenv.p.trigger_on_lsp);
2197 (* We deny all other requests. This is the only response that won't *)
2198 (* produce logs/warnings on most clients... *)
2199 raise (Error.RequestCancelled (lenv.p.new_hh_server_state |> hh_server_state_to_string))
2201 (* main: this is the main loop for processing incoming Lsp client requests,
2202 and incoming server notifications. Never returns. *)
2203 let main (env: env) : 'a =
2204 let open Marshal_tools in
2205 Printexc.record_backtrace true;
2206 HackEventLogger.client_set_from env.from;
2207 let client = Jsonrpc.make_queue () in
2208 let deferred_action = ref None in
2209 let state = ref Pre_init in
2210 while true do
2211 let ref_event = ref None in
2212 let ref_unblocked_time = ref (Unix.gettimeofday ()) in
2213 (* ref_unblocked_time is the time at which we're no longer blocked on either *)
2214 (* clientLsp message-loop or hh_server, and can start actually handling. *)
2215 (* Everything that blocks will update this variable. *)
2217 Option.call () !deferred_action;
2218 deferred_action := None;
2219 let event = get_next_event !state client in
2220 ref_event := Some event;
2221 ref_unblocked_time := Unix.gettimeofday ();
2223 (* maybe set a flag to indicate that we'll need to send an idle message *)
2224 state := handle_idle_if_necessary !state event;
2225 (* if we're in a lost-server state, some triggers cause us to reconnect *)
2226 state := reconnect_from_lost_if_necessary !state (`Event event);
2227 (* if the user does any interaction, then dismiss the "ready" dialog *)
2228 state := dismiss_ready_dialog_if_necessary !state event;
2229 (* we keep track of all open files and their contents *)
2230 state := track_open_files !state event;
2231 (* we keep track of all files that have unsaved changes in them *)
2232 state := track_edits_if_necessary !state event;
2233 (* if a message comes from the server, maybe update our record of server state *)
2234 update_hh_server_state_if_necessary event;
2236 (* this is the main handler for each message*)
2237 Jsonrpc.clear_last_sent ();
2238 handle_event ~env ~state ~client ~event ~ref_unblocked_time;
2239 let response = Jsonrpc.last_sent () in
2240 (* for LSP requests and notifications, we keep a log of what+when we responded *)
2241 log_response_if_necessary event response !ref_unblocked_time;
2242 with
2243 | Server_fatal_connection_exception edata ->
2244 if !state <> Post_shutdown then begin
2245 let stack = edata.stack ^ "---\n" ^ (Printexc.get_backtrace ()) in
2246 hack_log_error !ref_event edata.message stack "from_server" !ref_unblocked_time;
2247 Lsp_helpers.telemetry_error to_stdout (edata.message ^ ", from_server\n" ^ stack);
2248 (* The server never tells us why it closed the connection - it simply *)
2249 (* closes. We don't have privilege to inspect its exit status. *)
2250 (* The monitor is responsible for detecting server closure and exit *)
2251 (* status, and restarting the server if necessary (that's not our job). *)
2252 (* All we'll do is put up a dialog telling the user that the server is *)
2253 (* down and giving them a button to restart. We use a heuristic hint *)
2254 (* for when would be a good time to auto-dismiss the dialog and attempt *)
2255 (* a proper re-connection (it's not our job to ascertain with certainty *)
2256 (* whether that re-connection will succeed - it's impossible to know, *)
2257 (* but also our re-connection attempt is pretty forceful. Our heurstic *)
2258 (* is to sleep for 1 second, and then look for the presence of the lock *)
2259 (* file. The sleep is because typically if you do "hh stop" then the *)
2260 (* persistent connection shuts down instantly but the monitor takes a *)
2261 (* short time to release its lockfile. *)
2262 Unix.sleep 1;
2263 (* We're right now inside an exception handler. We don't want to do *)
2264 (* work that might itself throw. So instead we'll leave that to the *)
2265 (* next time around the loop. *)
2266 deferred_action := Some (fun () ->
2267 state := do_lost_server !state { Lost_env.
2268 explanation = Lost_env.Action_required "hh_server has stopped";
2269 new_hh_server_state = Hh_server_stopped;
2270 start_on_click = true;
2271 trigger_on_lock_file = true;
2272 trigger_on_lsp = false;
2275 | Client_fatal_connection_exception edata ->
2276 let stack = edata.stack ^ "---\n" ^ (Printexc.get_backtrace ()) in
2277 hack_log_error !ref_event edata.message stack "from_client" !ref_unblocked_time;
2278 Lsp_helpers.telemetry_error to_stdout (edata.message ^ ", from_client\n" ^ stack);
2279 exit_fail ()
2280 | Client_recoverable_connection_exception edata ->
2281 let stack = edata.stack ^ "---\n" ^ (Printexc.get_backtrace ()) in
2282 hack_log_error !ref_event edata.message stack "from_client" !ref_unblocked_time;
2283 Lsp_helpers.telemetry_error to_stdout (edata.message ^ ", from_client\n" ^ stack);
2284 | Server_nonfatal_exception edata ->
2285 let stack = edata.stack ^ "---\n" ^ (Printexc.get_backtrace ()) in
2286 hack_log_error !ref_event edata.message stack "from_server" !ref_unblocked_time;
2287 respond_to_error !ref_event (Error.Unknown edata.message) stack
2288 | e ->
2289 let message = Printexc.to_string e in
2290 let stack = Printexc.get_backtrace () in
2291 respond_to_error !ref_event e stack;
2292 hack_log_error !ref_event message stack "from_lsp" !ref_unblocked_time;
2293 done;
2294 failwith "unreachable"