use rpc instead of notify
[hiphop-php.git] / hphp / hack / src / client / clientLsp.ml
blobffb6bab5e14f2682a7e54365d45d8eafbde898b8
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_prelude
11 open Lsp
12 open Lsp_fmt
13 open Hh_json_helpers
15 (* All hack-specific code relating to LSP goes in here. *)
17 type env = {
18 from: string;
19 config: (string * string) list;
20 use_ffp_autocomplete: bool;
21 use_ranked_autocomplete: bool;
22 use_serverless_ide: bool;
23 verbose: bool;
26 (** This is env.from, but maybe modified in the light of the initialize request *)
27 let from = ref "[init]"
29 (** We cache the state of the typecoverageToggle button, so that when Hack restarts,
30 dynamic view stays in sync with the button in Nuclide *)
31 let cached_toggle_state = ref false
33 (************************************************************************)
34 (* Protocol orchestration & helpers *)
35 (************************************************************************)
37 (** We have an idea of server state based on what we hear from the server:
38 When we attempt a connection, we hear hopefully hear back that it's
39 INITIALIZING, and when we eventually receive "hello" that means it's
40 HANDLING_OR_READY, i.e. either handling a message, or ready to accept one.
41 But at connection attempt, we might see that it's STOPPED, or hear from it
42 that it's DENYING_CONNECTION (typically due to rebase).
43 When the server's running normally, we sometimes here push notifications to
44 tell us that it's TYPECHECKING, or has been STOLEN by another editor.
45 At any point of communication we might hear from the server that it
46 encountered a fatal exception, i.e. shutting down the pipe, so presumably
47 it has been STOPPED. When we reattempt to connect once a second, maybe we'll
48 get a better idea. *)
49 type hh_server_state =
50 | Hh_server_stopped
51 | Hh_server_initializing
52 | Hh_server_handling_or_ready
53 | Hh_server_denying_connection
54 | Hh_server_unknown
55 | Hh_server_typechecking_local
56 | Hh_server_typechecking_global_blocking
57 | Hh_server_typechecking_global_interruptible
58 | Hh_server_typechecking_global_remote_blocking
59 | Hh_server_stolen
60 | Hh_server_forgot
61 [@@deriving eq]
63 let hh_server_restart_button_text = "Restart hh_server"
65 let client_ide_restart_button_text = "Restart Hack IDE"
67 let see_output_hack = " See Output\xE2\x80\xBAHack for details." (* chevron *)
69 type incoming_metadata = {
70 timestamp: float; (** time this message arrived at stdin *)
71 tracking_id: string;
72 (** a unique random string of our own creation, which we can use for logging *)
75 (** A push message from the server might come while we're waiting for a server-rpc
76 response, or while we're free. The current architecture allows us to have
77 arbitrary responses to push messages while we're free, but only a limited set
78 of responses while we're waiting for a server-rpc - e.g. we can update our
79 notion of the server_state, or send a message to the client, but we can't
80 update our own state monad. The has_* fields are ad-hoc push-specific indicators
81 of whether we've done some part of the response during the rpc. *)
82 type server_message = {
83 push: ServerCommandTypes.push;
84 has_updated_server_state: bool;
87 type server_conn = {
88 ic: Timeout.in_channel;
89 oc: Out_channel.t;
90 server_finale_file: string;
91 pending_messages: server_message Queue.t;
92 (** ones that arrived during current rpc *)
95 module Main_env = struct
96 type t = {
97 conn: server_conn;
98 needs_idle: bool;
99 most_recent_file: documentUri option;
100 editor_open_files: Lsp.TextDocumentItem.t UriMap.t;
101 uris_with_diagnostics: UriSet.t;
102 uris_with_unsaved_changes: UriSet.t;
103 (** see comment in get_uris_with_unsaved_changes *)
104 hh_server_status: ShowStatusFB.params;
105 (** is updated by [handle_server_message] > [do_server_busy]. Shows status of
106 a connected hh_server, whether it's busy typechecking or ready:
107 (1) type_=InfoMessage when done typechecking, or WarningMessage during.
108 (2) shortMessage="Hack" if IDE is available, or "Hack: busy" if not
109 (3) message is a descriptive status about what it's doing. *)
113 module In_init_env = struct
114 type t = {
115 conn: server_conn;
116 first_start_time: float; (** our first attempt to connect *)
117 most_recent_start_time: float; (** for subsequent retries *)
118 most_recent_file: documentUri option;
119 editor_open_files: Lsp.TextDocumentItem.t UriMap.t;
120 uris_with_unsaved_changes: UriSet.t;
121 (** see comment in get_uris_with_unsaved_changes *)
122 hh_server_status_diagnostic: PublishDiagnostics.params option;
126 module Lost_env = struct
127 type t = {
128 p: params;
129 most_recent_file: documentUri option;
130 editor_open_files: Lsp.TextDocumentItem.t UriMap.t;
131 uris_with_unsaved_changes: UriSet.t;
132 (** see comment in get_uris_with_unsaved_changes *)
133 lock_file: string;
134 hh_server_status_diagnostic: PublishDiagnostics.params option;
137 and params = {
138 explanation: string;
139 new_hh_server_state: hh_server_state;
140 start_on_click: bool;
141 (** if user clicks Restart, do we ClientStart before reconnecting? *)
142 trigger_on_lsp: bool;
143 (** reconnect if we receive any LSP request/notification *)
144 trigger_on_lock_file: bool; (** reconnect if lockfile is created *)
148 type state =
149 | Pre_init (** Pre_init: we haven't yet received the initialize request. *)
150 | In_init of In_init_env.t
151 (** In_init: we did respond to the initialize request, and now we're
152 waiting for a "Hello" from the server. When that comes we'll
153 request a permanent connection from the server, and process the
154 file_changes backlog, and switch to Main_loop. *)
155 | Main_loop of Main_env.t
156 (** Main_loop: we have a working connection to both server and client. *)
157 | Lost_server of Lost_env.t
158 (** Lost_server: someone stole the persistent connection from us.
159 We might choose to grab it back if prompted... *)
160 | Post_shutdown
161 (** Post_shutdown: we received a shutdown request from the client, and
162 therefore shut down our connection to the server. We can't handle
163 any more requests from the client and will close as soon as it
164 notifies us that we can exit. *)
166 let is_post_shutdown (state : state) : bool =
167 match state with
168 | Post_shutdown -> true
169 | Pre_init
170 | In_init _
171 | Main_loop _
172 | Lost_server _ ->
173 false
175 let is_pre_init (state : state) : bool =
176 match state with
177 | Pre_init -> true
178 | Post_shutdown
179 | In_init _
180 | Main_loop _
181 | Lost_server _ ->
182 false
184 type result_handler = lsp_result -> state -> state Lwt.t
186 type result_telemetry = {
187 (* how many results did we send back to the user? *)
188 result_count: int;
189 (* other message-specific data *)
190 result_extra_telemetry: Telemetry.t option;
193 let initialize_params_ref : Lsp.Initialize.params option ref = ref None
195 let initialize_params_exc () : Lsp.Initialize.params =
196 match !initialize_params_ref with
197 | None -> failwith "initialize_params not yet received"
198 | Some initialize_params -> initialize_params
200 let get_root_opt () : Path.t option =
201 match !initialize_params_ref with
202 | None -> None
203 | Some initialize_params ->
204 let path = Some (Lsp_helpers.get_root initialize_params) in
205 Some (Wwwroot.get path)
207 let get_root_exn () : Path.t = Option.value_exn (get_root_opt ())
209 let hhconfig_version : string ref = ref "[NotYetInitialized]"
211 (** This flag is used to control how much will be written
212 to log-files. It can be turned on initially by --verbose at the command-line or
213 setting "trace:Verbose" in initializationParams. Thereafter, it can
214 be changed by the user dynamically via $/setTraceNotification.
215 Don't alter this reference directly; instead use [set_verbose_to_file]
216 so as to pass the message on to ide_service as well.
217 Note: control for how much will be written to stderr is solely
218 controlled by --verbose at the command-line, stored in env.verbose. *)
219 let verbose_to_file : bool ref = ref false
221 let can_autostart_after_mismatch : bool ref = ref true
223 let requests_outstanding : (lsp_request * result_handler) IdMap.t ref =
224 ref IdMap.empty
226 let get_outstanding_request_exn (id : lsp_id) : lsp_request =
227 match IdMap.find_opt id !requests_outstanding with
228 | Some (request, _) -> request
229 | None -> failwith "response id doesn't correspond to an outstanding request"
231 (* head is newest *)
232 let hh_server_state : (float * hh_server_state) list ref = ref []
234 let showStatus_outstanding : string ref = ref ""
236 let log s = Hh_logger.log ("[client-lsp] " ^^ s)
238 let log_debug s = Hh_logger.debug ("[client-lsp] " ^^ s)
240 let to_stdout (json : Hh_json.json) : unit =
241 let s = Hh_json.json_to_string json ^ "\r\n\r\n" in
242 Http_lite.write_message stdout s
244 let get_editor_open_files (state : state) :
245 Lsp.TextDocumentItem.t UriMap.t option =
246 match state with
247 | Pre_init
248 | Post_shutdown ->
249 None
250 | Main_loop menv -> Some menv.Main_env.editor_open_files
251 | In_init ienv -> Some ienv.In_init_env.editor_open_files
252 | Lost_server lenv -> Some lenv.Lost_env.editor_open_files
254 (** This is the most recent file that was subject of an LSP request
255 from the client. There's no guarantee that the file is still open. *)
256 let get_most_recent_file (state : state) : documentUri option =
257 match state with
258 | Pre_init
259 | Post_shutdown ->
260 None
261 | Main_loop menv -> menv.Main_env.most_recent_file
262 | In_init ienv -> ienv.In_init_env.most_recent_file
263 | Lost_server lenv -> lenv.Lost_env.most_recent_file
265 type event =
266 | Server_hello
267 | Server_message of server_message
268 | Client_message of incoming_metadata * lsp_message
269 (** Client_message stores raw json, and the parsed form of it *)
270 | Client_ide_notification of ClientIdeMessage.notification
271 | Tick (** once per second, on idle *)
273 let is_tick = function
274 | Tick -> true
275 | Server_hello
276 | Server_message _
277 | Client_message _
278 | Client_ide_notification _ ->
279 false
281 (* Here are some exit points. *)
282 let exit_ok () = exit 0
284 let exit_fail () = exit 1
286 (* The following connection exceptions inform the main LSP event loop how to
287 respond to an exception: was the exception a connection-related exception
288 (one of these) or did it arise during other logic (not one of these)? Can
289 we report the exception to the LSP client? Can we continue handling
290 further LSP messages or must we quit? If we quit, can we do so immediately
291 or must we delay? -- Separately, they also help us marshal callstacks
292 across daemon- and process-boundaries. *)
294 exception
295 Client_fatal_connection_exception of Marshal_tools.remote_exception_data
297 exception
298 Client_recoverable_connection_exception of Marshal_tools.remote_exception_data
300 exception
301 Server_fatal_connection_exception of Marshal_tools.remote_exception_data
303 exception Server_nonfatal_exception of Lsp.Error.t
305 let state_to_string (state : state) : string =
306 match state with
307 | Pre_init -> "Pre_init"
308 | In_init _ienv -> "In_init"
309 | Main_loop _menv -> "Main_loop"
310 | Lost_server _lenv -> "Lost_server"
311 | Post_shutdown -> "Post_shutdown"
313 let hh_server_state_to_string (hh_server_state : hh_server_state) : string =
314 match hh_server_state with
315 | Hh_server_denying_connection -> "hh_server denying connection"
316 | Hh_server_initializing -> "hh_server initializing"
317 | Hh_server_stopped -> "hh_server stopped"
318 | Hh_server_stolen -> "hh_server stolen"
319 | Hh_server_typechecking_local -> "hh_server typechecking (local)"
320 | Hh_server_typechecking_global_blocking ->
321 "hh_server typechecking (global, blocking)"
322 | Hh_server_typechecking_global_interruptible ->
323 "hh_server typechecking (global, interruptible)"
324 | Hh_server_typechecking_global_remote_blocking ->
325 "hh_server typechecking (global remote, blocking)"
326 | Hh_server_handling_or_ready -> "hh_server ready"
327 | Hh_server_unknown -> "hh_server unknown state"
328 | Hh_server_forgot -> "hh_server forgotten state"
330 (** This conversion is imprecise. Comments indicate potential gaps *)
331 let completion_kind_to_si_kind
332 (completion_kind : Completion.completionItemKind option) :
333 SearchUtils.si_kind =
334 let open Lsp in
335 let open SearchUtils in
336 match completion_kind with
337 | Some Completion.Class -> SI_Class
338 | Some Completion.Method -> SI_ClassMethod
339 | Some Completion.Function -> SI_Function
340 | Some Completion.Variable ->
341 SI_LocalVariable (* or SI_Mixed, but that's never used *)
342 | Some Completion.Property -> SI_Property
343 | Some Completion.Constant -> SI_GlobalConstant (* or SI_ClassConstant *)
344 | Some Completion.Interface -> SI_Interface (* or SI_Trait *)
345 | Some Completion.Enum -> SI_Enum
346 | Some Completion.Module -> SI_Namespace
347 | Some Completion.Constructor -> SI_Constructor
348 | Some Completion.Keyword -> SI_Keyword
349 | Some Completion.Value -> SI_Literal
350 | Some Completion.TypeParameter -> SI_Typedef
351 (* The completion enum includes things we don't really support *)
352 | _ -> SI_Unknown
354 let si_kind_to_completion_kind (kind : SearchUtils.si_kind) :
355 Completion.completionItemKind option =
356 match kind with
357 | SearchUtils.SI_XHP
358 | SearchUtils.SI_Class ->
359 Some Completion.Class
360 | SearchUtils.SI_ClassMethod -> Some Completion.Method
361 | SearchUtils.SI_Function -> Some Completion.Function
362 | SearchUtils.SI_Mixed
363 | SearchUtils.SI_LocalVariable ->
364 Some Completion.Variable
365 | SearchUtils.SI_Property -> Some Completion.Property
366 | SearchUtils.SI_ClassConstant -> Some Completion.Constant
367 | SearchUtils.SI_Interface
368 | SearchUtils.SI_Trait ->
369 Some Completion.Interface
370 | SearchUtils.SI_Enum -> Some Completion.Enum
371 | SearchUtils.SI_Namespace -> Some Completion.Module
372 | SearchUtils.SI_Constructor -> Some Completion.Constructor
373 | SearchUtils.SI_Keyword -> Some Completion.Keyword
374 | SearchUtils.SI_Literal -> Some Completion.Value
375 | SearchUtils.SI_GlobalConstant -> Some Completion.Constant
376 | SearchUtils.SI_Typedef -> Some Completion.TypeParameter
377 | SearchUtils.SI_RecordDef -> Some Completion.Struct
378 | SearchUtils.SI_Unknown -> None
380 (** We keep a log of server state over the past 2mins. When adding a new server
381 state: if this state is the same as the current one, then ignore it. Also,
382 retain only states younger than 2min plus the first one older than 2min.
383 Newest state is at head of list. *)
384 let set_hh_server_state (new_hh_server_state : hh_server_state) : unit =
385 let new_time = Unix.gettimeofday () in
386 let rec retain rest =
387 match rest with
388 | [] -> []
389 | (time, state) :: rest when time >= new_time -. 120.0 ->
390 (time, state) :: retain rest
391 | (time, state) :: _rest -> [(time, state)]
392 (* retain only the first that's older *)
394 hh_server_state :=
395 match !hh_server_state with
396 | (prev_time, prev_hh_server_state) :: rest
397 when equal_hh_server_state prev_hh_server_state new_hh_server_state ->
398 (prev_time, prev_hh_server_state) :: retain rest
399 | rest -> (new_time, new_hh_server_state) :: retain rest
401 let get_older_hh_server_state (requested_time : float) : hh_server_state =
402 (* find the first item which is older than the specified time. *)
403 match
404 List.find !hh_server_state ~f:(fun (time, _) -> time <= requested_time)
405 with
406 | None -> Hh_server_forgot
407 | Some (_, hh_server_state) -> hh_server_state
409 let read_hhconfig_version () : string Lwt.t =
410 match get_root_opt () with
411 | None -> Lwt.return "[NoRoot]"
412 | Some root ->
413 let file = Filename.concat (Path.to_string root) ".hhconfig" in
414 let%lwt config = Config_file_lwt.parse_hhconfig file in
415 (match config with
416 | Ok (_hash, config) ->
417 let version =
418 SMap.find_opt "version" config
419 |> Config_file_lwt.parse_version
420 |> Config_file_lwt.version_to_string_opt
421 |> Option.value ~default:"[NoVersion]"
423 Lwt.return version
424 | Error message -> Lwt.return (Printf.sprintf "[NoHhconfig:%s]" message))
426 (** get_uris_with_unsaved_changes is the set of files for which we've
427 received didChange but haven't yet received didSave/didOpen. It is purely
428 a description of what we've heard of the editor, and is independent of
429 whether or not they've yet been synced with hh_server.
430 As it happens: in Main_loop state all these files will already have been
431 sent to hh_server; in In_init state all these files will have been queued
432 up inside editor_open_files ready to be sent when we receive the hello; in
433 Lost_server state they're not even queued up, and if ever we see hh_server
434 ready then we'll terminate the LSP server and trust the client to relaunch
435 us and resend a load of didOpen/didChange events. *)
436 let get_uris_with_unsaved_changes (state : state) : UriSet.t =
437 match state with
438 | Main_loop menv -> menv.Main_env.uris_with_unsaved_changes
439 | In_init ienv -> ienv.In_init_env.uris_with_unsaved_changes
440 | Lost_server lenv -> lenv.Lost_env.uris_with_unsaved_changes
441 | _ -> UriSet.empty
443 let update_hh_server_state_if_necessary (event : event) : unit =
444 let open ServerCommandTypes in
445 let helper push =
446 match push with
447 | BUSY_STATUS Needs_local_typecheck
448 | BUSY_STATUS Done_local_typecheck
449 | BUSY_STATUS (Done_global_typecheck _) ->
450 set_hh_server_state Hh_server_handling_or_ready
451 | BUSY_STATUS Doing_local_typecheck ->
452 set_hh_server_state Hh_server_typechecking_local
453 | BUSY_STATUS (Doing_global_typecheck global_typecheck_kind) ->
454 set_hh_server_state
455 (match global_typecheck_kind with
456 | Blocking -> Hh_server_typechecking_global_blocking
457 | Interruptible -> Hh_server_typechecking_global_interruptible
458 | Remote_blocking _ -> Hh_server_typechecking_global_remote_blocking)
459 | NEW_CLIENT_CONNECTED -> set_hh_server_state Hh_server_stolen
460 | DIAGNOSTIC _
461 | FATAL_EXCEPTION _
462 | NONFATAL_EXCEPTION _ ->
465 match event with
466 | Server_message { push; has_updated_server_state = false } -> helper push
467 | _ -> ()
469 let rpc_lock = Lwt_mutex.create ()
471 let rpc
472 (server_conn : server_conn)
473 (ref_unblocked_time : float ref)
474 (command : 'a ServerCommandTypes.t) : 'a Lwt.t =
475 let%lwt result =
476 Lwt_mutex.with_lock rpc_lock (fun () ->
477 let callback () push =
478 update_hh_server_state_if_necessary
479 (Server_message { push; has_updated_server_state = false });
480 Queue.enqueue
481 server_conn.pending_messages
482 { push; has_updated_server_state = true }
484 let start_time = Unix.gettimeofday () in
485 let%lwt result =
486 ServerCommandLwt.rpc_persistent
487 (server_conn.ic, server_conn.oc)
489 callback
490 command
492 let end_time = Unix.gettimeofday () in
493 let duration = end_time -. start_time in
494 let msg = ServerCommandTypesUtils.debug_describe_t command in
495 log_debug "hh_server rpc: [%s] [%0.3f]" msg duration;
496 match result with
497 | Ok ((), res, start_server_handle_time) ->
498 ref_unblocked_time := start_server_handle_time;
499 Lwt.return res
500 | Error
501 ( (),
502 Utils.Callstack _,
503 ServerCommandLwt.Remote_fatal_exception remote_e_data ) ->
504 raise (Server_fatal_connection_exception remote_e_data)
505 | Error
506 ( (),
507 Utils.Callstack _,
508 ServerCommandLwt.Remote_nonfatal_exception
509 { Marshal_tools.message; stack } ) ->
510 let lsp_error =
512 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
513 message;
514 data = Lsp_fmt.error_data_of_stack stack;
517 raise (Server_nonfatal_exception lsp_error)
518 | Error ((), Utils.Callstack stack, e) ->
519 let message = Exn.to_string e in
520 raise
521 (Server_fatal_connection_exception { Marshal_tools.message; stack }))
523 Lwt.return result
525 let rpc_with_retry server_conn ref_unblocked_time command =
526 ServerCommandTypes.Done_or_retry.call ~f:(fun () ->
527 rpc server_conn ref_unblocked_time command)
529 (** A thin wrapper around ClientIdeMessage which turns errors into exceptions *)
530 let ide_rpc
531 (ide_service : ClientIdeService.t)
532 ~(tracking_id : string)
533 ~(ref_unblocked_time : float ref)
534 ~(needs_init : bool)
535 (message : 'a ClientIdeMessage.t) : 'a Lwt.t =
536 let%lwt result =
537 ClientIdeService.rpc
538 ide_service
539 ~tracking_id
540 ~ref_unblocked_time
541 ~needs_init
542 message
544 match result with
545 | Ok result -> Lwt.return result
546 | Error edata -> raise (Server_nonfatal_exception edata)
548 let set_verbose_to_file
549 ~(ide_service : ClientIdeService.t option)
550 ~(tracking_id : string)
551 (value : bool) : unit =
552 verbose_to_file := value;
553 if !verbose_to_file then
554 Hh_logger.Level.set_min_level_file Hh_logger.Level.Debug
555 else
556 Hh_logger.Level.set_min_level_file Hh_logger.Level.Info;
557 match ide_service with
558 | Some ide_service ->
559 let ref_unblocked_time = ref 0. in
560 Lwt.async (fun () ->
561 try%lwt
562 let%lwt () =
563 ide_rpc
564 ide_service
565 ~tracking_id
566 ~ref_unblocked_time
567 ~needs_init:false
568 (ClientIdeMessage.Verbose !verbose_to_file)
570 Lwt.return_unit
571 with _exn -> Lwt.return_unit
572 (* TODO: log this *));
574 | None -> ()
576 (** Determine whether to read a message from the client (the editor) or the
577 server (hh_server), or whether neither is ready within 1s. *)
578 let get_message_source (server : server_conn) (client : Jsonrpc.queue) :
579 [ `From_server | `From_client | `From_ide_service of event | `No_source ]
580 Lwt.t =
581 (* Take action on server messages in preference to client messages, because
582 server messages are very easy and quick to service (just send a message to
583 the client), while client messages require us to launch a potentially
584 long-running RPC command. *)
585 let has_server_messages = not (Queue.is_empty server.pending_messages) in
586 if has_server_messages then
587 Lwt.return `From_server
588 else if Jsonrpc.has_message client then
589 Lwt.return `From_client
590 else
591 (* If no immediate messages are available, then wait up to 1 second. *)
592 let server_read_fd =
593 Unix.descr_of_out_channel server.oc |> Lwt_unix.of_unix_file_descr
595 let client_read_fd =
596 Jsonrpc.get_read_fd client |> Lwt_unix.of_unix_file_descr
598 let%lwt message_source =
599 Lwt.pick
601 (let%lwt () = Lwt_unix.sleep 1.0 in
602 Lwt.return `No_source);
603 (* Note that `wait_read` waits for the file descriptor to be readable, but
604 does not actually read anything from it (so we won't end up with a race
605 condition where we've read data from both file descriptors but only process
606 the data from either the client or the server). *)
607 (let%lwt () = Lwt_unix.wait_read server_read_fd in
608 Lwt.return `From_server);
609 (let%lwt () = Lwt_unix.wait_read client_read_fd in
610 Lwt.return `From_client);
613 Lwt.return message_source
615 (** A simplified version of get_message_source which only looks at client *)
616 let get_client_message_source
617 (client : Jsonrpc.queue) (ide_service : ClientIdeService.t option) :
618 [ `From_client | `From_ide_service of event | `No_source ] Lwt.t =
619 if Jsonrpc.has_message client then
620 Lwt.return `From_client
621 else
622 let client_read_fd =
623 Jsonrpc.get_read_fd client |> Lwt_unix.of_unix_file_descr
625 let pop_from_ide_service =
626 match ide_service with
627 | None -> Lwt.wait () |> fst (* a never-fulfilled promise *)
628 | Some ide_service ->
629 Lwt_message_queue.pop (ClientIdeService.get_notifications ide_service)
631 let%lwt message_source =
632 Lwt.pick
634 (let%lwt () = Lwt_unix.sleep 1.0 in
635 Lwt.return `No_source);
636 (let%lwt () = Lwt_unix.wait_read client_read_fd in
637 Lwt.return `From_client);
638 (let%lwt notification = pop_from_ide_service in
639 match notification with
640 | None ->
641 let%lwt () = Lwt_unix.sleep 1.1 in
642 failwith "should have deferred to the `No_source case above"
643 | Some message ->
644 Lwt.return (`From_ide_service (Client_ide_notification message)));
647 Lwt.return message_source
649 (** Read a message unmarshaled from the server's out_channel. *)
650 let read_message_from_server (server : server_conn) : event Lwt.t =
651 let open ServerCommandTypes in
652 try%lwt
653 let fd =
654 Unix.descr_of_out_channel server.oc |> Lwt_unix.of_unix_file_descr
656 let%lwt (message : 'a ServerCommandTypes.message_type) =
657 Marshal_tools_lwt.from_fd_with_preamble fd
659 match message with
660 | Response _ -> failwith "unexpected response without request"
661 | Push push ->
662 Lwt.return (Server_message { push; has_updated_server_state = false })
663 | Hello -> Lwt.return Server_hello
664 | Ping -> failwith "unexpected ping on persistent connection"
665 with e ->
666 let message = Exn.to_string e in
667 let stack = Printexc.get_backtrace () in
668 raise (Server_fatal_connection_exception { Marshal_tools.message; stack })
670 (** get_next_event: picks up the next available message from either client or
671 server. The way it's implemented, at the first character of a message
672 from either client or server, we block until that message is completely
673 received. Note: if server is None (meaning we haven't yet established
674 connection with server) then we'll just block waiting for client. *)
675 let get_next_event
676 (state : state)
677 (client : Jsonrpc.queue)
678 (ide_service : ClientIdeService.t option) : event Lwt.t =
679 let from_server (server : server_conn) : event Lwt.t =
680 if Queue.is_empty server.pending_messages then
681 read_message_from_server server
682 else
683 Lwt.return (Server_message (Queue.dequeue_exn server.pending_messages))
685 let from_client (client : Jsonrpc.queue) : event Lwt.t =
686 let%lwt message = Jsonrpc.get_message client in
687 match message with
688 | `Message { Jsonrpc.json; timestamp } ->
689 begin
691 let message = Lsp_fmt.parse_lsp json get_outstanding_request_exn in
692 let rnd = Random_id.short_string () in
693 let tracking_id =
694 match message with
695 | RequestMessage (id, _) -> rnd ^ "." ^ Lsp_fmt.id_to_string id
696 | _ -> rnd
698 Lwt.return (Client_message ({ tracking_id; timestamp }, message))
699 with e ->
700 let e = Exception.wrap e in
701 let edata =
703 Marshal_tools.stack = Exception.get_backtrace_string e;
704 message = Exception.get_ctor_string e;
707 raise (Client_recoverable_connection_exception edata)
709 | `Fatal_exception edata -> raise (Client_fatal_connection_exception edata)
710 | `Recoverable_exception edata ->
711 raise (Client_recoverable_connection_exception edata)
713 match state with
714 | Main_loop { Main_env.conn; _ }
715 | In_init { In_init_env.conn; _ } ->
716 let%lwt message_source = get_message_source conn client in
717 (match message_source with
718 | `From_client ->
719 let%lwt message = from_client client in
720 Lwt.return message
721 | `From_server ->
722 let%lwt message = from_server conn in
723 Lwt.return message
724 | `From_ide_service message -> Lwt.return message
725 | `No_source -> Lwt.return Tick)
726 | _ ->
727 let%lwt message_source = get_client_message_source client ide_service in
728 (match message_source with
729 | `From_client ->
730 let%lwt message = from_client client in
731 Lwt.return message
732 | `From_ide_service message -> Lwt.return message
733 | `No_source -> Lwt.return Tick)
735 type powered_by =
736 | Hh_server
737 | Language_server
738 | Serverless_ide
740 let add_powered_by ~(powered_by : powered_by) (json : Hh_json.json) :
741 Hh_json.json =
742 let open Hh_json in
743 match (json, powered_by) with
744 | (JSON_Object props, Serverless_ide) ->
745 JSON_Object (("powered_by", JSON_String "serverless_ide") :: props)
746 | (_, _) -> json
748 let respond_jsonrpc
749 ~(powered_by : powered_by) (id : lsp_id) (result : lsp_result) : unit =
750 print_lsp_response id result |> add_powered_by ~powered_by |> to_stdout
752 let notify_jsonrpc ~(powered_by : powered_by) (notification : lsp_notification)
753 : unit =
754 print_lsp_notification notification |> add_powered_by ~powered_by |> to_stdout
756 (** respond_to_error: if we threw an exception during the handling of a request,
757 report the exception to the client as the response to their request. *)
758 let respond_to_error (event : event option) (e : Lsp.Error.t) : unit =
759 let result = ErrorResult e in
760 match event with
761 | Some (Client_message (_, RequestMessage (id, _request))) ->
762 respond_jsonrpc ~powered_by:Language_server id result
763 | _ ->
764 (* We want to report LSP error 'e' over jsonrpc. But jsonrpc only allows
765 errors to be reported in response to requests. So we'll stick the information
766 in a telemetry/event. The format of this event isn't defined. We're going to
767 roll our own, using ad-hoc json fields to emit all the data out of 'e' *)
768 let open Lsp.Error in
769 let extras =
770 ("code", e.code |> Error.show_code |> Hh_json.string_)
771 :: Option.value_map e.data ~default:[] ~f:(fun data -> [("data", data)])
773 Lsp_helpers.telemetry_error to_stdout e.message ~extras
775 (** request_showStatusFB: pops up a dialog *)
776 let request_showStatusFB
777 ?(on_result : ShowStatusFB.result -> state -> state Lwt.t =
778 (fun _ state -> Lwt.return state))
779 ?(on_error : Error.t -> state -> state Lwt.t =
780 (fun _ state -> Lwt.return state))
781 (params : ShowStatusFB.params) : unit =
782 let initialize_params = initialize_params_exc () in
783 if not (Lsp_helpers.supports_status initialize_params) then
785 else
786 (* We try not to send duplicate statuses.
787 That means: if you call request_showStatus but your message is the same as
788 what's already up, then you won't be shown, and your callbacks won't be shown. *)
789 let msg = params.ShowStatusFB.request.ShowMessageRequest.message in
790 if String.equal msg !showStatus_outstanding then
792 else (
793 showStatus_outstanding := msg;
794 let id = NumberId (Jsonrpc.get_next_request_id ()) in
795 let request = ShowStatusRequestFB params in
796 to_stdout (print_lsp_request id request);
798 let handler (result : lsp_result) (state : state) : state Lwt.t =
799 if String.equal msg !showStatus_outstanding then
800 showStatus_outstanding := "";
801 match result with
802 | ShowStatusResultFB result -> on_result result state
803 | ErrorResult error -> on_error error state
804 | _ ->
805 let error =
807 Error.code = Error.ParseError;
808 message = "expected ShowStatusResult";
809 data = None;
812 on_error error state
814 requests_outstanding :=
815 IdMap.add id (request, handler) !requests_outstanding
818 (** request_showMessage: pops up a dialog *)
819 let request_showMessage
820 (on_result : ShowMessageRequest.result -> state -> state Lwt.t)
821 (on_error : Error.t -> state -> state Lwt.t)
822 (type_ : MessageType.t)
823 (message : string)
824 (titles : string list) : ShowMessageRequest.t =
825 (* send the request *)
826 let id = NumberId (Jsonrpc.get_next_request_id ()) in
827 let actions =
828 List.map titles ~f:(fun title -> { ShowMessageRequest.title })
830 let request =
831 ShowMessageRequestRequest { ShowMessageRequest.type_; message; actions }
833 to_stdout (print_lsp_request id request);
835 let handler (result : lsp_result) (state : state) : state Lwt.t =
836 match result with
837 | ShowMessageRequestResult result -> on_result result state
838 | ErrorResult error -> on_error error state
839 | _ ->
840 let error =
842 Error.code = Error.ParseError;
843 message = "expected ShowMessageRequestResult";
844 data = None;
847 on_error error state
849 requests_outstanding := IdMap.add id (request, handler) !requests_outstanding;
851 (* return a token *)
852 ShowMessageRequest.Present { id }
854 (** dismiss_showMessageRequest: sends a cancellation-request for the dialog *)
855 let dismiss_showMessageRequest (dialog : ShowMessageRequest.t) :
856 ShowMessageRequest.t =
857 begin
858 match dialog with
859 | ShowMessageRequest.Absent -> ()
860 | ShowMessageRequest.Present { id; _ } ->
861 let notification = CancelRequestNotification { CancelRequest.id } in
862 let json = Lsp_fmt.print_lsp (NotificationMessage notification) in
863 to_stdout json
864 end;
865 ShowMessageRequest.Absent
867 (** These functions are not currently used, but may be useful in the future. *)
868 let (_ : 'a -> 'b) = request_showMessage
870 and (_ : 'c -> 'd) = dismiss_showMessageRequest
872 (** dismiss_diagnostics: dismisses all diagnostics from a state,
873 both the error diagnostics in Main_loop and the hh_server_status
874 diagnostics in In_init and Lost_server. *)
875 let dismiss_diagnostics (state : state) : state =
876 let dismiss_one ~isStatusFB uri =
877 let params = { PublishDiagnostics.uri; diagnostics = []; isStatusFB } in
878 let notification = PublishDiagnosticsNotification params in
879 notification |> print_lsp_notification |> to_stdout
881 let dismiss_status diagnostic =
882 dismiss_one ~isStatusFB:true diagnostic.PublishDiagnostics.uri
884 match state with
885 | In_init ienv ->
886 let open In_init_env in
887 Option.iter ienv.hh_server_status_diagnostic ~f:dismiss_status;
888 In_init { ienv with hh_server_status_diagnostic = None }
889 | Main_loop menv ->
890 let open Main_env in
891 UriSet.iter (dismiss_one ~isStatusFB:false) menv.uris_with_diagnostics;
892 Main_loop { menv with uris_with_diagnostics = UriSet.empty }
893 | Lost_server lenv ->
894 let open Lost_env in
895 Option.iter lenv.hh_server_status_diagnostic ~f:dismiss_status;
896 Lost_server { lenv with hh_server_status_diagnostic = None }
897 | Pre_init -> Pre_init
898 | Post_shutdown -> Post_shutdown
900 (************************************************************************)
901 (* Conversions - ad-hoc ones written as needed them, not systematic *)
902 (************************************************************************)
904 let lsp_uri_to_path = Lsp_helpers.lsp_uri_to_path
906 let path_to_lsp_uri = Lsp_helpers.path_to_lsp_uri
908 let lsp_position_to_ide (position : Lsp.position) : Ide_api_types.position =
909 { Ide_api_types.line = position.line + 1; column = position.character + 1 }
911 let lsp_file_position_to_hack (params : Lsp.TextDocumentPositionParams.t) :
912 string * int * int =
913 let open Lsp.TextDocumentPositionParams in
914 let { Ide_api_types.line; column } = lsp_position_to_ide params.position in
915 let filename =
916 Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument
918 (filename, line, column)
920 let rename_params_to_document_position (params : Lsp.Rename.params) :
921 Lsp.TextDocumentPositionParams.t =
922 Rename.
924 TextDocumentPositionParams.textDocument = params.textDocument;
925 position = params.position;
928 let hack_pos_to_lsp_range ~(equal : 'a -> 'a -> bool) (pos : 'a Pos.pos) :
929 Lsp.range =
930 (* .hhconfig errors are Positions with a filename, but dummy start/end
931 * positions. Handle that case - and Pos.none - specially, as the LSP
932 * specification requires line and character >= 0, and VSCode silently
933 * drops diagnostics that violate the spec in this way *)
934 if Pos.equal_pos equal pos (Pos.make_from (Pos.filename pos)) then
935 { start = { line = 0; character = 0 }; end_ = { line = 0; character = 0 } }
936 else
937 let (line1, col1, line2, col2) = Pos.destruct_range pos in
939 start = { line = line1 - 1; character = col1 - 1 };
940 end_ = { line = line2 - 1; character = col2 - 1 };
943 let hack_pos_to_lsp_location (pos : Pos.absolute) ~(default_path : string) :
944 Lsp.Location.t =
945 Lsp.Location.
947 uri = path_to_lsp_uri (Pos.filename pos) ~default_path;
948 range = hack_pos_to_lsp_range ~equal:String.equal pos;
951 let ide_range_to_lsp (range : Ide_api_types.range) : Lsp.range =
953 Lsp.start =
955 Lsp.line = range.Ide_api_types.st.Ide_api_types.line - 1;
956 character = range.Ide_api_types.st.Ide_api_types.column - 1;
958 end_ =
960 Lsp.line = range.Ide_api_types.ed.Ide_api_types.line - 1;
961 character = range.Ide_api_types.ed.Ide_api_types.column - 1;
965 let lsp_range_to_ide (range : Lsp.range) : Ide_api_types.range =
966 Ide_api_types.
968 st = lsp_position_to_ide range.start;
969 ed = lsp_position_to_ide range.end_;
972 let hack_symbol_definition_to_lsp_construct_location
973 (symbol : string SymbolDefinition.t) ~(default_path : string) :
974 Lsp.Location.t =
975 let open SymbolDefinition in
976 hack_pos_to_lsp_location symbol.span ~default_path
978 let hack_pos_definition_to_lsp_identifier_location
979 (sid : Pos.absolute * string) ~(default_path : string) :
980 Lsp.DefinitionLocation.t =
981 let (pos, title) = sid in
982 let location = hack_pos_to_lsp_location pos ~default_path in
983 Lsp.DefinitionLocation.{ location; title = Some title }
985 let hack_symbol_definition_to_lsp_identifier_location
986 (symbol : string SymbolDefinition.t) ~(default_path : string) :
987 Lsp.DefinitionLocation.t =
988 let open SymbolDefinition in
989 let location = hack_pos_to_lsp_location symbol.pos ~default_path in
990 Lsp.DefinitionLocation.
992 location;
993 title = Some (Utils.strip_ns symbol.SymbolDefinition.full_name);
996 let hack_errors_to_lsp_diagnostic
997 (filename : string) (errors : Pos.absolute Errors.error_ list) :
998 PublishDiagnostics.params =
999 let open Lsp.Location in
1000 let location_message (error : Pos.absolute * string) : Lsp.Location.t * string
1002 let (pos, message) = error in
1003 let { uri; range } = hack_pos_to_lsp_location pos ~default_path:filename in
1004 ({ Location.uri; range }, message)
1006 let hack_error_to_lsp_diagnostic (error : Pos.absolute Errors.error_) =
1007 let all_messages = Errors.to_list error |> List.map ~f:location_message in
1008 let (first_message, additional_messages) =
1009 match all_messages with
1010 | hd :: tl -> (hd, tl)
1011 | [] -> failwith "Expected at least one error in the error list"
1013 let ({ range; _ }, message) = first_message in
1014 let relatedInformation =
1015 additional_messages
1016 |> List.map ~f:(fun (location, message) ->
1018 PublishDiagnostics.relatedLocation = location;
1019 relatedMessage = message;
1022 let severity =
1023 match Errors.get_severity error with
1024 | Errors.Error -> Some PublishDiagnostics.Error
1025 | Errors.Warning -> Some PublishDiagnostics.Warning
1028 Lsp.PublishDiagnostics.range;
1029 severity;
1030 code = PublishDiagnostics.IntCode (Errors.get_code error);
1031 source = Some "Hack";
1032 message;
1033 relatedInformation;
1034 relatedLocations = relatedInformation (* legacy FB extension *);
1037 (* The caller is required to give us a non-empty filename. If it is empty, *)
1038 (* the following path_to_lsp_uri will fall back to the default path - which *)
1039 (* is also empty - and throw, logging appropriate telemetry. *)
1041 Lsp.PublishDiagnostics.uri = path_to_lsp_uri filename ~default_path:"";
1042 isStatusFB = false;
1043 diagnostics = List.map errors ~f:hack_error_to_lsp_diagnostic;
1046 (************************************************************************)
1047 (* Protocol *)
1048 (************************************************************************)
1049 let get_document_contents
1050 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t) (uri : documentUri) :
1051 string option =
1052 match UriMap.find_opt uri editor_open_files with
1053 | Some document -> Some document.TextDocumentItem.text
1054 | None -> None
1056 let get_document_location
1057 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1058 (params : Lsp.TextDocumentPositionParams.t) :
1059 ClientIdeMessage.document_location =
1060 let (file_path, line, column) = lsp_file_position_to_hack params in
1061 let uri =
1062 params.TextDocumentPositionParams.textDocument.TextDocumentIdentifier.uri
1064 let file_path = Path.make file_path in
1065 let file_contents = get_document_contents editor_open_files uri in
1066 { ClientIdeMessage.file_path; file_contents; line; column }
1068 let stop_ide_service
1069 (ide_service : ClientIdeService.t)
1070 ~(tracking_id : string)
1071 ~(reason : ClientIdeService.Stop_reason.t) : unit Lwt.t =
1073 "Stopping IDE service process: %s"
1074 (ClientIdeService.Stop_reason.to_string reason);
1075 let%lwt () = ClientIdeService.stop ide_service ~tracking_id ~reason in
1076 Lwt.return_unit
1078 let do_shutdown
1079 (state : state)
1080 (ide_service : ClientIdeService.t option)
1081 (tracking_id : string)
1082 (ref_unblocked_time : float ref) : state Lwt.t =
1083 log "Received shutdown request";
1084 let state = dismiss_diagnostics state in
1085 let%lwt () =
1086 match state with
1087 | Main_loop menv ->
1088 (* In Main_loop state, we're expected to unsubscribe diagnostics and tell *)
1089 (* server to disconnect so it can revert the state of its unsaved files. *)
1090 Main_env.(
1091 log "Diag_subscribe: clientLsp do_shutdown unsubscribing diagnostic 0 ";
1092 let%lwt () =
1094 menv.conn
1095 ref_unblocked_time
1096 (ServerCommandTypes.UNSUBSCRIBE_DIAGNOSTIC 0)
1098 let%lwt () = rpc menv.conn (ref 0.0) ServerCommandTypes.DISCONNECT in
1099 Lwt.return_unit)
1100 | In_init _ienv ->
1101 (* In In_init state, even though we have a 'conn', it's still waiting for *)
1102 (* the server to become responsive, so there's no use sending any rpc *)
1103 (* messages to the server over it. *)
1104 Lwt.return_unit
1105 | _ ->
1106 (* No other states have a 'conn' to send any disconnect messages over. *)
1107 Lwt.return_unit
1108 and () =
1109 match ide_service with
1110 | None -> Lwt.return_unit
1111 | Some ide_service ->
1112 stop_ide_service
1113 ide_service
1114 ~tracking_id
1115 ~reason:ClientIdeService.Stop_reason.Editor_exited
1117 Lwt.return Post_shutdown
1119 let state_to_rage (state : state) : string =
1120 let details =
1121 match state with
1122 | Pre_init -> []
1123 | Post_shutdown -> []
1124 | Main_loop menv ->
1125 Main_env.
1127 "needs_idle";
1128 menv.needs_idle |> string_of_bool;
1129 "editor_open_files";
1130 menv.editor_open_files |> UriMap.keys |> List.length |> string_of_int;
1131 "uris_with_diagnostics";
1132 menv.uris_with_diagnostics |> UriSet.cardinal |> string_of_int;
1133 "uris_with_unsaved_changes";
1134 menv.uris_with_unsaved_changes |> UriSet.cardinal |> string_of_int;
1135 "hh_server_status.message";
1136 menv.hh_server_status.ShowStatusFB.request.ShowMessageRequest.message;
1137 "hh_server_status.shortMessage";
1138 Option.value
1139 menv.hh_server_status.ShowStatusFB.shortMessage
1140 ~default:"";
1142 | In_init ienv ->
1143 In_init_env.
1145 "first_start_time";
1146 ienv.first_start_time |> string_of_float;
1147 "most_recent_start_time";
1148 ienv.most_recent_start_time |> string_of_float;
1149 "editor_open_files";
1150 ienv.editor_open_files |> UriMap.keys |> List.length |> string_of_int;
1151 "uris_with_unsaved_changes";
1152 ienv.uris_with_unsaved_changes |> UriSet.cardinal |> string_of_int;
1154 | Lost_server lenv ->
1155 Lost_env.
1157 "editor_open_files";
1158 lenv.editor_open_files |> UriMap.keys |> List.length |> string_of_int;
1159 "uris_with_unsaved_changes";
1160 lenv.uris_with_unsaved_changes |> UriSet.cardinal |> string_of_int;
1161 "lock_file";
1162 lenv.lock_file;
1163 "explanation";
1164 lenv.p.explanation;
1165 "new_hh_server_state";
1166 lenv.p.new_hh_server_state |> hh_server_state_to_string;
1167 "start_on_click";
1168 lenv.p.start_on_click |> string_of_bool;
1169 "trigger_on_lsp";
1170 lenv.p.trigger_on_lsp |> string_of_bool;
1171 "trigger_on_lock_file";
1172 lenv.p.trigger_on_lock_file |> string_of_bool;
1175 state_to_string state ^ "\n" ^ String.concat ~sep:"\n" details ^ "\n"
1177 let do_rageFB (state : state) (ref_unblocked_time : float ref) :
1178 RageFB.result Lwt.t =
1179 RageFB.(
1180 let items : rageItem list ref = ref [] in
1181 let add item = items := item :: !items in
1182 let add_data data = add { title = None; data } in
1183 let add_fn fn =
1184 if Sys.file_exists fn then
1185 add { title = Some fn; data = Sys_utils.cat fn }
1187 let get_stack (pid, reason) : string Lwt.t =
1188 let pid = string_of_int pid in
1189 let format_data msg : string Lwt.t =
1190 Lwt.return (Printf.sprintf "PSTACK %s (%s) - %s\n\n" pid reason msg)
1192 log "Getting pstack for %s" pid;
1193 match%lwt Lwt_utils.exec_checked Exec_command.Pstack [| pid |] with
1194 | Ok result ->
1195 let stack = result.Lwt_utils.Process_success.stdout in
1196 format_data stack
1197 | Error _ ->
1198 (* pstack is just an alias for gstack, but it's not present on all systems. *)
1199 log "Failed to execute pstack for %s. Executing gstack instead" pid;
1200 (match%lwt Lwt_utils.exec_checked Exec_command.Gstack [| pid |] with
1201 | Ok result ->
1202 let stack = result.Lwt_utils.Process_success.stdout in
1203 format_data stack
1204 | Error e ->
1205 let err =
1206 "unable to get pstack - " ^ e.Lwt_utils.Process_failure.stderr
1208 format_data err)
1210 (* logfiles. Start them, but don't wait yet because we want this to run concurrently with fetching
1211 * the server logs. *)
1212 let get_log_files =
1213 match get_root_opt () with
1214 | Some root ->
1215 add_fn (ServerFiles.log_link root);
1216 add_fn (ServerFiles.log_link root ^ ".old");
1217 add_fn (ServerFiles.monitor_log_link root);
1218 add_fn (ServerFiles.monitor_log_link root ^ ".old");
1219 add_fn (ServerFiles.client_lsp_log root);
1220 add_fn (ServerFiles.client_lsp_log root ^ ".old");
1221 add_fn (ServerFiles.client_ide_log root);
1222 add_fn (ServerFiles.client_ide_log root ^ ".old");
1223 (try%lwt
1224 let pids = PidLog.get_pids (ServerFiles.pids_file root) in
1225 let is_interesting (_, reason) =
1226 not (String_utils.string_starts_with reason "slave")
1228 let%lwt stacks =
1229 Lwt.pick
1231 (let%lwt () = Lwt_unix.sleep 4.50 in
1232 Lwt.return ["Timed out while getting pstacks"]);
1233 pids
1234 |> List.filter ~f:is_interesting
1235 |> Lwt_list.map_p get_stack;
1238 List.iter stacks ~f:add_data;
1239 Lwt.return_unit
1240 with e ->
1241 let message = Exn.to_string e in
1242 let stack = Printexc.get_backtrace () in
1243 Lwt.return
1244 (add_data
1245 (Printf.sprintf "Failed to get PIDs: %s - %s" message stack)))
1246 | None -> Lwt.return_unit
1248 (* client *)
1249 add_data ("LSP adapter state: " ^ state_to_rage state ^ "\n");
1251 (* client: version *)
1252 let current_version = read_hhconfig_version () in
1253 (* client's log of server state *)
1254 let tnow = Unix.gettimeofday () in
1255 let server_state_to_string (tstate, state) =
1256 let open Unix in
1257 let tdiff = tnow -. tstate in
1258 let state = hh_server_state_to_string state in
1259 let tm = Unix.localtime tstate in
1260 let ms = int_of_float (tstate *. 1000.) mod 1000 in
1261 Printf.sprintf
1262 "[%02d:%02d:%02d.%03d] [%03.3fs ago] %s\n"
1263 tm.tm_hour
1264 tm.tm_min
1265 tm.tm_sec
1267 tdiff
1268 state
1270 let server_state_strings =
1271 List.map ~f:server_state_to_string !hh_server_state
1273 add_data
1274 (String.concat
1275 ~sep:""
1276 ("LSP belief of hh_server_state:\n" :: server_state_strings));
1278 (* server *)
1279 let server_promise =
1280 match state with
1281 | Main_loop menv ->
1282 Main_env.(
1283 let%lwt items =
1284 rpc menv.conn ref_unblocked_time ServerCommandTypes.RAGE
1286 let add i =
1288 { title = i.ServerRageTypes.title; data = i.ServerRageTypes.data }
1290 List.iter items ~f:add;
1291 Lwt.return (Ok ()))
1292 | _ -> Lwt.return (Error "server rage - not in main loop")
1294 let timeout_promise =
1295 let%lwt () = Lwt_unix.sleep 30. in
1296 (* 30s *)
1297 Lwt.return (Error "server rage - timeout 30s")
1299 let%lwt server_rage_result =
1300 try%lwt Lwt.pick [server_promise; timeout_promise]
1301 with e ->
1302 let message = Exn.to_string e in
1303 let stack = Printexc.get_backtrace () in
1304 Lwt.return (Error (Printf.sprintf "server rage - %s\n%s" message stack))
1306 (* Don't start waiting on these until the end because we want all of our LWT requests to be in
1307 * flight simultaneously. *)
1308 let%lwt () = get_log_files in
1309 let%lwt current_version = current_version in
1310 add_data ("Version previously read from .hhconfig: " ^ !hhconfig_version);
1311 add_data ("Version in .hhconfig: " ^ current_version);
1313 Str.string_match
1314 (Str.regexp "^\\^[0-9]+\\.[0-9]+\\.[0-9]+")
1315 current_version
1317 then
1318 add_data
1319 ( "Version source control: hg update remote/releases/hack/v"
1320 ^ String_utils.lstrip current_version "^" );
1321 Result.iter_error server_rage_result ~f:add_data;
1323 (* that's it! *)
1324 Lwt.return !items)
1326 let do_toggleTypeCoverageFB
1327 (conn : server_conn)
1328 (ref_unblocked_time : float ref)
1329 (params : ToggleTypeCoverageFB.params) : unit Lwt.t =
1330 (* Currently, the only thing to do on toggling type coverage is turn on dynamic view *)
1331 let command =
1332 ServerCommandTypes.DYNAMIC_VIEW params.ToggleTypeCoverageFB.toggle
1334 cached_toggle_state := params.ToggleTypeCoverageFB.toggle;
1335 rpc conn ref_unblocked_time command
1337 let do_didOpen
1338 (conn : server_conn)
1339 (ref_unblocked_time : float ref)
1340 (params : DidOpen.params) : unit Lwt.t =
1341 let open DidOpen in
1342 let open TextDocumentItem in
1343 let filename = lsp_uri_to_path params.textDocument.uri in
1344 let text = params.textDocument.text in
1345 let command = ServerCommandTypes.OPEN_FILE (filename, text) in
1346 rpc conn ref_unblocked_time command
1348 let do_didClose
1349 (conn : server_conn)
1350 (ref_unblocked_time : float ref)
1351 (params : DidClose.params) : unit Lwt.t =
1352 let open DidClose in
1353 let open TextDocumentIdentifier in
1354 let filename = lsp_uri_to_path params.textDocument.uri in
1355 let command = ServerCommandTypes.CLOSE_FILE filename in
1356 rpc conn ref_unblocked_time command
1358 let do_didChange
1359 (conn : server_conn)
1360 (ref_unblocked_time : float ref)
1361 (params : DidChange.params) : unit Lwt.t =
1362 let open VersionedTextDocumentIdentifier in
1363 let open Lsp.DidChange in
1364 let lsp_change_to_ide (lsp : DidChange.textDocumentContentChangeEvent) :
1365 Ide_api_types.text_edit =
1367 Ide_api_types.range = Option.map lsp.range lsp_range_to_ide;
1368 text = lsp.text;
1371 let filename = lsp_uri_to_path params.textDocument.uri in
1372 let changes = List.map params.contentChanges ~f:lsp_change_to_ide in
1373 let command = ServerCommandTypes.EDIT_FILE (filename, changes) in
1374 rpc conn ref_unblocked_time command
1376 let do_hover_common (infos : HoverService.hover_info list) : Hover.result =
1377 let contents =
1378 infos
1379 |> List.map ~f:(fun hoverInfo ->
1380 (* Hack server uses None to indicate absence of a result. *)
1381 (* We're also catching the non-result "" just in case... *)
1382 match hoverInfo with
1383 | { HoverService.snippet = ""; _ } -> []
1384 | { HoverService.snippet; addendum; _ } ->
1385 MarkedCode ("hack", snippet)
1386 :: List.map ~f:(fun s -> MarkedString s) addendum)
1387 |> List.concat
1389 (* We pull the position from the SymbolOccurrence.t record, so I would be
1390 surprised if there were any different ones in here. Just take the first
1391 non-None one. *)
1392 let range =
1393 infos
1394 |> List.filter_map ~f:(fun { HoverService.pos; _ } -> pos)
1395 |> List.hd
1396 |> Option.map ~f:(hack_pos_to_lsp_range ~equal:Relative_path.equal)
1398 if List.is_empty contents then
1399 None
1400 else
1401 Some { Hover.contents; range }
1403 let do_hover
1404 (conn : server_conn)
1405 (ref_unblocked_time : float ref)
1406 (params : Hover.params) : Hover.result Lwt.t =
1407 let (file, line, column) = lsp_file_position_to_hack params in
1408 let command = ServerCommandTypes.IDE_HOVER (file, line, column) in
1409 let%lwt infos = rpc conn ref_unblocked_time command in
1410 Lwt.return (do_hover_common infos)
1412 let do_hover_local
1413 (ide_service : ClientIdeService.t)
1414 (tracking_id : string)
1415 (ref_unblocked_time : float ref)
1416 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1417 (params : Hover.params) : Hover.result Lwt.t =
1418 let document_location = get_document_location editor_open_files params in
1419 let%lwt infos =
1420 ClientIdeService.rpc
1421 ide_service
1422 ~tracking_id
1423 ~ref_unblocked_time
1424 ~needs_init:true
1425 (ClientIdeMessage.Hover document_location)
1427 match infos with
1428 | Ok infos ->
1429 let infos = do_hover_common infos in
1430 Lwt.return infos
1431 | Error edata -> raise (Server_nonfatal_exception edata)
1433 let do_typeDefinition
1434 (conn : server_conn)
1435 (ref_unblocked_time : float ref)
1436 (params : Definition.params) : TypeDefinition.result Lwt.t =
1437 let (file, line, column) = lsp_file_position_to_hack params in
1438 let command =
1439 ServerCommandTypes.(IDENTIFY_TYPES (LabelledFileName file, line, column))
1441 let%lwt results = rpc conn ref_unblocked_time command in
1442 Lwt.return
1443 (List.map results ~f:(fun nast_sid ->
1444 hack_pos_definition_to_lsp_identifier_location
1445 nast_sid
1446 ~default_path:file))
1448 let do_typeDefinition_local
1449 (ide_service : ClientIdeService.t)
1450 (tracking_id : string)
1451 (ref_unblocked_time : float ref)
1452 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1453 (params : Definition.params) : TypeDefinition.result Lwt.t =
1454 let document_location = get_document_location editor_open_files params in
1455 let%lwt results =
1456 ClientIdeService.rpc
1457 ide_service
1458 ~tracking_id
1459 ~ref_unblocked_time
1460 ~needs_init:true
1461 (ClientIdeMessage.Type_definition document_location)
1463 match results with
1464 | Ok results ->
1465 let file = Path.to_string document_location.ClientIdeMessage.file_path in
1466 let results =
1467 List.map results ~f:(fun nast_sid ->
1468 hack_pos_definition_to_lsp_identifier_location
1469 nast_sid
1470 ~default_path:file)
1472 Lwt.return results
1473 | Error edata -> raise (Server_nonfatal_exception edata)
1475 let do_definition
1476 (conn : server_conn)
1477 (ref_unblocked_time : float ref)
1478 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1479 (params : Definition.params) : Definition.result Lwt.t =
1480 let (filename, line, column) = lsp_file_position_to_hack params in
1481 let uri =
1482 params.TextDocumentPositionParams.textDocument.TextDocumentIdentifier.uri
1484 let labelled_file =
1485 match UriMap.find_opt uri editor_open_files with
1486 | Some document ->
1487 ServerCommandTypes.(
1488 LabelledFileContent
1489 { filename; content = document.TextDocumentItem.text })
1490 | None -> ServerCommandTypes.(LabelledFileName filename)
1492 let command =
1493 ServerCommandTypes.GO_TO_DEFINITION (labelled_file, line, column)
1495 let%lwt results = rpc conn ref_unblocked_time command in
1496 Lwt.return
1497 (List.map results ~f:(fun (_occurrence, definition) ->
1498 hack_symbol_definition_to_lsp_identifier_location
1499 definition
1500 ~default_path:filename))
1502 let do_definition_local
1503 (ide_service : ClientIdeService.t)
1504 (tracking_id : string)
1505 (ref_unblocked_time : float ref)
1506 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1507 (params : Definition.params) : Definition.result Lwt.t =
1508 let document_location = get_document_location editor_open_files params in
1509 let%lwt results =
1510 ClientIdeService.rpc
1511 ide_service
1512 ~tracking_id
1513 ~ref_unblocked_time
1514 ~needs_init:true
1515 (ClientIdeMessage.Definition document_location)
1517 match results with
1518 | Ok results ->
1519 let results =
1520 List.map results ~f:(fun (_occurrence, definition) ->
1521 hack_symbol_definition_to_lsp_identifier_location
1522 definition
1523 ~default_path:
1524 (document_location.ClientIdeMessage.file_path |> Path.to_string))
1526 Lwt.return results
1527 | Error edata -> raise (Server_nonfatal_exception edata)
1529 let snippet_re = Str.regexp {|[\$}]|} (* snippets must backslash-escape "$\}" *)
1531 let make_ide_completion_response
1532 (result : AutocompleteTypes.ide_result) (filename : string) :
1533 Completion.completionList Lwt.t =
1534 let open AutocompleteTypes in
1535 let open Completion in
1536 (* We use snippets to provide parentheses+arguments when autocompleting *)
1537 (* method calls e.g. "$c->|" ==> "$c->foo($arg1)". But we'll only do this *)
1538 (* there's nothing after the caret: no "$c->|(1)" -> "$c->foo($arg1)(1)" *)
1539 let is_caret_followed_by_lparen = Char.equal result.char_at_pos '(' in
1540 let p = initialize_params_exc () in
1541 let hack_to_itemType (completion : complete_autocomplete_result) :
1542 string option =
1543 (* TODO: we're using itemType (left column) for function return types, and *)
1544 (* the inlineDetail (right column) for variable/field types. Is that good? *)
1545 Option.map completion.func_details ~f:(fun details -> details.return_ty)
1547 let hack_to_detail (completion : complete_autocomplete_result) : string =
1548 (* TODO: retrieve the actual signature including name+modifiers *)
1549 (* For now we just return the type of the completion. In the case *)
1550 (* of functions, their function-types have parentheses around them *)
1551 (* which we want to strip. In other cases like tuples, no strip. *)
1552 match completion.func_details with
1553 | None -> completion.res_ty
1554 | Some _ ->
1555 String_utils.rstrip (String_utils.lstrip completion.res_ty "(") ")"
1557 let hack_to_inline_detail (completion : complete_autocomplete_result) : string
1559 match completion.func_details with
1560 | None -> hack_to_detail completion
1561 | Some details ->
1562 (* "(type1 $param1, ...)" *)
1563 let f param = Printf.sprintf "%s %s" param.param_ty param.param_name in
1564 let params = String.concat ~sep:", " (List.map details.params ~f) in
1565 Printf.sprintf "(%s)" params
1566 (* Returns a tuple of (insertText, insertTextFormat, textEdits). *)
1568 let hack_to_insert (completion : complete_autocomplete_result) :
1569 [ `InsertText of string | `TextEdit of TextEdit.t list ]
1570 * Completion.insertTextFormat =
1571 let use_textedits =
1572 Initialize.(p.initializationOptions.useTextEditAutocomplete)
1574 match (completion.func_details, use_textedits) with
1575 | (Some details, _)
1576 when Lsp_helpers.supports_snippets p
1577 && (not is_caret_followed_by_lparen)
1578 && not
1579 (SearchUtils.equal_si_kind
1580 completion.res_kind
1581 SearchUtils.SI_LocalVariable) ->
1582 (* "method(${1:arg1}, ...)" but for args we just use param names. *)
1583 let f i param =
1584 let name = Str.global_replace snippet_re "\\\\\\0" param.param_name in
1585 Printf.sprintf "${%i:%s}" (i + 1) name
1587 let params = String.concat ~sep:", " (List.mapi details.params ~f) in
1588 ( `InsertText (Printf.sprintf "%s(%s)" completion.res_name params),
1589 SnippetFormat )
1590 | (_, false) -> (`InsertText completion.res_name, PlainText)
1591 | (_, true) ->
1592 ( `TextEdit
1594 TextEdit.
1596 range = ide_range_to_lsp completion.res_replace_pos;
1597 newText = completion.res_name;
1600 PlainText )
1602 let hack_completion_to_lsp (completion : complete_autocomplete_result) :
1603 Completion.completionItem =
1604 let (insertText, insertTextFormat, textEdits) =
1605 match hack_to_insert completion with
1606 | (`InsertText text, format) -> (Some text, format, [])
1607 | (`TextEdit edits, format) -> (None, format, edits)
1609 let pos =
1610 if String.equal (Pos.filename completion.res_pos) "" then
1611 Pos.set_file filename completion.res_pos
1612 else
1613 completion.res_pos
1615 let data =
1616 let (line, start, _) = Pos.info_pos pos in
1617 let filename = Pos.filename pos in
1618 let base_class =
1619 match completion.res_base_class with
1620 | Some base_class -> [("base_class", Hh_json.JSON_String base_class)]
1621 | None -> []
1623 let ranking_detail =
1624 match completion.ranking_details with
1625 | Some details ->
1627 ("ranking_detail", Hh_json.JSON_String details.detail);
1628 ("ranking_source", Hh_json.JSON_Number details.kind);
1630 | None -> []
1632 (* If we do not have a correct file position, skip sending that data *)
1633 if Int.equal line 0 && Int.equal start 0 then
1634 Some
1635 (Hh_json.JSON_Object
1636 ( [("fullname", Hh_json.JSON_String completion.res_fullname)]
1637 @ base_class
1638 @ ranking_detail ))
1639 else
1640 Some
1641 (Hh_json.JSON_Object
1643 (* Fullname is needed for namespaces. We often trim namespaces to make
1644 * the results more readable, such as showing "ad__breaks" instead of
1645 * "Thrift\Packages\cf\ad__breaks".
1647 ("fullname", Hh_json.JSON_String completion.res_fullname);
1648 (* Filename/line/char/base_class are used to handle class methods.
1649 * We could unify this with fullname in the future.
1651 ("filename", Hh_json.JSON_String filename);
1652 ("line", Hh_json.int_ line);
1653 ("char", Hh_json.int_ start);
1655 @ base_class
1656 @ ranking_detail ))
1658 let hack_to_sort_text (completion : complete_autocomplete_result) :
1659 string option =
1660 let label = completion.res_name in
1661 let should_downrank label =
1662 String.length label > 2
1663 && String.equal (Str.string_before label 2) "__"
1664 || Str.string_match (Str.regexp_case_fold ".*do_not_use.*") label 0
1666 let downranked_result_prefix_character = "~" in
1667 if should_downrank label then
1668 Some (downranked_result_prefix_character ^ label)
1669 else
1670 Some label
1673 label =
1674 ( completion.res_name
1677 SearchUtils.equal_si_kind completion.res_kind SearchUtils.SI_Namespace
1678 then
1679 "\\"
1680 else
1681 "" );
1682 kind =
1683 (match completion.ranking_details with
1684 | Some _ -> Some Completion.Event
1685 | None ->
1686 si_kind_to_completion_kind completion.AutocompleteTypes.res_kind);
1687 detail = Some (hack_to_detail completion);
1688 inlineDetail = Some (hack_to_inline_detail completion);
1689 itemType = hack_to_itemType completion;
1690 documentation = None;
1691 (* This will be filled in by completionItem/resolve. *)
1692 sortText =
1693 (match completion.ranking_details with
1694 | Some detail -> Some detail.sort_text
1695 | None -> hack_to_sort_text completion);
1696 filterText = None;
1697 insertText;
1698 insertTextFormat = Some insertTextFormat;
1699 textEdits;
1700 command = None;
1701 data;
1704 Lwt.return
1706 isIncomplete = not result.is_complete;
1707 items = List.map result.completions ~f:hack_completion_to_lsp;
1710 let do_completion_ffp
1711 (conn : server_conn)
1712 (ref_unblocked_time : float ref)
1713 (params : Completion.params) : Completion.result Lwt.t =
1714 let open Completion in
1715 let open TextDocumentIdentifier in
1716 let pos =
1717 lsp_position_to_ide params.loc.TextDocumentPositionParams.position
1719 let filename =
1720 lsp_uri_to_path params.loc.TextDocumentPositionParams.textDocument.uri
1722 let command = ServerCommandTypes.IDE_FFP_AUTOCOMPLETE (filename, pos) in
1723 let%lwt result = rpc conn ref_unblocked_time command in
1724 make_ide_completion_response result filename
1726 let do_completion_legacy
1727 (conn : server_conn)
1728 (ref_unblocked_time : float ref)
1729 (params : Completion.params) : Completion.result Lwt.t =
1730 let open Completion in
1731 let open TextDocumentIdentifier in
1732 let pos =
1733 lsp_position_to_ide params.loc.TextDocumentPositionParams.position
1735 let filename =
1736 lsp_uri_to_path params.loc.TextDocumentPositionParams.textDocument.uri
1738 let is_manually_invoked =
1739 match params.context with
1740 | None -> false
1741 | Some c -> is_invoked c.triggerKind
1743 let command =
1744 ServerCommandTypes.IDE_AUTOCOMPLETE (filename, pos, is_manually_invoked)
1746 let%lwt result = rpc conn ref_unblocked_time command in
1747 make_ide_completion_response result filename
1749 let do_completion_local
1750 (ide_service : ClientIdeService.t)
1751 (tracking_id : string)
1752 (ref_unblocked_time : float ref)
1753 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1754 (params : Completion.params) : Completion.result Lwt.t =
1755 let open Completion in
1756 let document_location = get_document_location editor_open_files params.loc in
1757 (* Other parameters *)
1758 let is_manually_invoked =
1759 match params.context with
1760 | None -> false
1761 | Some c -> is_invoked c.triggerKind
1763 (* this is what I want to fix *)
1764 let request =
1765 ClientIdeMessage.Completion
1766 { ClientIdeMessage.Completion.document_location; is_manually_invoked }
1768 let%lwt result =
1769 ClientIdeService.rpc
1770 ide_service
1771 ~tracking_id
1772 ~ref_unblocked_time
1773 ~needs_init:true
1774 request
1776 match result with
1777 | Ok infos ->
1778 let filename =
1779 document_location.ClientIdeMessage.file_path |> Path.to_string
1781 let%lwt response = make_ide_completion_response infos filename in
1782 Lwt.return response
1783 | Error edata -> raise (Server_nonfatal_exception edata)
1785 exception NoLocationFound
1787 let docblock_to_markdown (raw_docblock : DocblockService.result) :
1788 markedString list option =
1789 match raw_docblock with
1790 | [] -> None
1791 | docblock ->
1792 Some
1793 (Core_kernel.List.fold docblock ~init:[] ~f:(fun acc elt ->
1794 match elt with
1795 | DocblockService.Markdown txt -> MarkedString txt :: acc
1796 | DocblockService.HackSnippet txt -> MarkedCode ("hack", txt) :: acc
1797 | DocblockService.XhpSnippet txt -> MarkedCode ("html", txt) :: acc))
1799 let docblock_with_ranking_detail
1800 (raw_docblock : DocblockService.result) (ranking_detail : string option) :
1801 DocblockService.result =
1802 match ranking_detail with
1803 | Some detail -> raw_docblock @ [DocblockService.Markdown detail]
1804 | None -> raw_docblock
1806 let resolve_ranking_source
1807 (kind : SearchUtils.si_kind) (ranking_source : int option) :
1808 SearchUtils.si_kind =
1809 match ranking_source with
1810 | Some x -> SearchUtils.int_to_kind x
1811 | None -> kind
1813 let do_completionItemResolve
1814 (conn : server_conn)
1815 (ref_unblocked_time : float ref)
1816 (params : CompletionItemResolve.params) : CompletionItemResolve.result Lwt.t
1818 (* No matter what, we need the kind *)
1819 let raw_kind = params.Completion.kind in
1820 let kind = completion_kind_to_si_kind raw_kind in
1821 (* First try fetching position data from json *)
1822 let%lwt raw_docblock =
1824 match params.Completion.data with
1825 | None -> raise NoLocationFound
1826 | Some _ as data ->
1827 (* Some docblocks are for class methods. Class methods need to know
1828 * file/line/column/base_class to find the docblock. *)
1829 let filename = Jget.string_exn data "filename" in
1830 let line = Jget.int_exn data "line" in
1831 let column = Jget.int_exn data "char" in
1832 let base_class = Jget.string_opt data "base_class" in
1833 let ranking_detail = Jget.string_opt data "ranking_detail" in
1834 let ranking_source = Jget.int_opt data "ranking_source" in
1835 (* If not found ... *)
1836 if line = 0 && column = 0 then (
1837 (* For global symbols such as functions, classes, enums, etc, we
1838 * need to know the full name INCLUDING all namespaces. Once
1839 * we know that, we can look up its file/line/column. *)
1840 let fullname = Jget.string_exn data "fullname" in
1841 if String.equal fullname "" then raise NoLocationFound;
1842 let fullname = Utils.add_ns fullname in
1843 let command =
1844 ServerCommandTypes.DOCBLOCK_FOR_SYMBOL
1845 (fullname, resolve_ranking_source kind ranking_source)
1847 let%lwt raw_docblock = rpc conn ref_unblocked_time command in
1848 Lwt.return (docblock_with_ranking_detail raw_docblock ranking_detail)
1849 ) else
1850 (* Okay let's get a docblock for this specific location *)
1851 let command =
1852 ServerCommandTypes.DOCBLOCK_AT
1853 ( filename,
1854 line,
1855 column,
1856 base_class,
1857 resolve_ranking_source kind ranking_source )
1859 let%lwt raw_docblock = rpc conn ref_unblocked_time command in
1860 Lwt.return (docblock_with_ranking_detail raw_docblock ranking_detail)
1861 (* If that failed, fetch docblock using just the symbol name *)
1862 with _ ->
1863 let symbolname = params.Completion.label in
1864 let ranking_source =
1865 try Jget.int_opt params.Completion.data "ranking_source"
1866 with _ -> None
1868 let command =
1869 ServerCommandTypes.DOCBLOCK_FOR_SYMBOL
1870 (symbolname, resolve_ranking_source kind ranking_source)
1872 let%lwt raw_docblock = rpc conn ref_unblocked_time command in
1873 Lwt.return raw_docblock
1875 (* Convert to markdown and return *)
1876 let documentation = docblock_to_markdown raw_docblock in
1877 Lwt.return { params with Completion.documentation }
1880 * Note that resolve does not depend on having previously executed completion in
1881 * the same process. The LSP resolve request takes, as input, a single item
1882 * produced by any previously executed completion request. So it's okay for
1883 * one process to respond to another, because they'll both know the answers
1884 * to the same symbol requests.
1886 * And it's totally okay to mix and match requests to serverless IDE and
1887 * hh_server.
1889 let do_resolve_local
1890 (ide_service : ClientIdeService.t)
1891 (tracking_id : string)
1892 (ref_unblocked_time : float ref)
1893 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
1894 (params : CompletionItemResolve.params) : CompletionItemResolve.result Lwt.t
1896 let raw_kind = params.Completion.kind in
1897 let kind = completion_kind_to_si_kind raw_kind in
1898 (* Some docblocks are for class methods. Class methods need to know
1899 * file/line/column/base_class to find the docblock. *)
1900 let%lwt result =
1902 match params.Completion.data with
1903 | None -> raise NoLocationFound
1904 | Some _ as data ->
1905 let filename = Jget.string_exn data "filename" in
1906 let uri = File_url.create filename |> Lsp.uri_of_string in
1907 let file_path = Path.make filename in
1908 let line = Jget.int_exn data "line" in
1909 let column = Jget.int_exn data "char" in
1910 let file_contents = get_document_contents editor_open_files uri in
1911 let ranking_detail = Jget.string_opt data "ranking_detail" in
1912 let ranking_source = Jget.int_opt data "ranking_source" in
1913 if line = 0 && column = 0 then failwith "NoFileLineColumnData";
1914 let request =
1915 ClientIdeMessage.Completion_resolve_location
1917 ClientIdeMessage.Completion_resolve_location.document_location =
1919 ClientIdeMessage.file_path;
1920 ClientIdeMessage.file_contents;
1921 ClientIdeMessage.line;
1922 ClientIdeMessage.column;
1924 kind = resolve_ranking_source kind ranking_source;
1927 let%lwt location_result =
1928 ClientIdeService.rpc
1929 ide_service
1930 ~tracking_id
1931 ~ref_unblocked_time
1932 ~needs_init:true
1933 request
1935 (match location_result with
1936 | Ok raw_docblock ->
1937 let documentation =
1938 docblock_with_ranking_detail raw_docblock ranking_detail
1939 |> docblock_to_markdown
1941 Lwt.return { params with Completion.documentation }
1942 | Error edata -> raise (Server_nonfatal_exception edata))
1943 (* If that fails, next try using symbol *)
1944 with _ ->
1945 (* The "fullname" value includes the fully qualified namespace, so
1946 * we want to use that. However, if it's missing (it shouldn't be)
1947 * let's default to using the label which doesn't include the
1948 * namespace. *)
1949 let symbolname =
1950 try Jget.string_exn params.Completion.data "fullname"
1951 with _ -> params.Completion.label
1953 let ranking_source =
1954 try Jget.int_opt params.Completion.data "ranking_source"
1955 with _ -> None
1957 let request =
1958 ClientIdeMessage.Completion_resolve
1960 ClientIdeMessage.Completion_resolve.symbol = symbolname;
1961 kind = resolve_ranking_source kind ranking_source;
1964 let%lwt resolve_result =
1965 ClientIdeService.rpc
1966 ide_service
1967 ~tracking_id
1968 ~ref_unblocked_time
1969 ~needs_init:true
1970 request
1972 (match resolve_result with
1973 | Ok raw_docblock ->
1974 let documentation = docblock_to_markdown raw_docblock in
1975 Lwt.return { params with Completion.documentation }
1976 | Error edata -> raise (Server_nonfatal_exception edata))
1978 Lwt.return result
1980 let do_workspaceSymbol
1981 (conn : server_conn)
1982 (ref_unblocked_time : float ref)
1983 (params : WorkspaceSymbol.params) : WorkspaceSymbol.result Lwt.t =
1984 let open WorkspaceSymbol in
1985 let open SearchUtils in
1986 let query = params.query in
1987 let query_type = "" in
1988 let command = ServerCommandTypes.SEARCH (query, query_type) in
1989 let%lwt results = rpc conn ref_unblocked_time command in
1990 let hack_to_lsp_kind = function
1991 | SearchUtils.SI_Class -> SymbolInformation.Class
1992 | SearchUtils.SI_Interface -> SymbolInformation.Interface
1993 | SearchUtils.SI_Trait -> SymbolInformation.Interface
1994 (* LSP doesn't have traits, so we approximate with interface *)
1995 | SearchUtils.SI_Enum -> SymbolInformation.Enum
1996 (* TODO(T36697624): Add SymbolInformation.Record *)
1997 | SearchUtils.SI_ClassMethod -> SymbolInformation.Method
1998 | SearchUtils.SI_Function -> SymbolInformation.Function
1999 | SearchUtils.SI_Typedef -> SymbolInformation.Class
2000 (* LSP doesn't have typedef, so we approximate with class *)
2001 | SearchUtils.SI_GlobalConstant -> SymbolInformation.Constant
2002 | SearchUtils.SI_Namespace -> SymbolInformation.Namespace
2003 | SearchUtils.SI_Mixed -> SymbolInformation.Variable
2004 | SearchUtils.SI_XHP -> SymbolInformation.Class
2005 | SearchUtils.SI_Literal -> SymbolInformation.Variable
2006 | SearchUtils.SI_ClassConstant -> SymbolInformation.Constant
2007 | SearchUtils.SI_Property -> SymbolInformation.Property
2008 | SearchUtils.SI_LocalVariable -> SymbolInformation.Variable
2009 | SearchUtils.SI_Constructor -> SymbolInformation.Constructor
2010 | SearchUtils.SI_RecordDef -> SymbolInformation.Struct
2011 (* Do these happen in practice? *)
2012 | SearchUtils.SI_Keyword
2013 | SearchUtils.SI_Unknown ->
2014 failwith "Unknown symbol kind"
2016 (* Hack sometimes gives us back items with an empty path, by which it *)
2017 (* intends "whichever path you asked me about". That would be meaningless *)
2018 (* here. If it does, then it'll pick up our default path (also empty), *)
2019 (* which will throw and go into our telemetry. That's the best we can do. *)
2020 let hack_symbol_to_lsp (symbol : SearchUtils.symbol) =
2022 SymbolInformation.name = Utils.strip_ns symbol.name;
2023 kind = hack_to_lsp_kind symbol.result_type;
2024 location = hack_pos_to_lsp_location symbol.pos ~default_path:"";
2025 containerName = None;
2028 Lwt.return (List.map results ~f:hack_symbol_to_lsp)
2030 let rec hack_symbol_tree_to_lsp
2031 ~(filename : string)
2032 ~(accu : Lsp.SymbolInformation.t list)
2033 ~(container_name : string option)
2034 (defs : FileOutline.outline) : Lsp.SymbolInformation.t list =
2035 let open SymbolDefinition in
2036 let hack_to_lsp_kind = function
2037 | SymbolDefinition.Function -> SymbolInformation.Function
2038 | SymbolDefinition.Class -> SymbolInformation.Class
2039 | SymbolDefinition.Method -> SymbolInformation.Method
2040 | SymbolDefinition.Property -> SymbolInformation.Property
2041 | SymbolDefinition.RecordDef -> SymbolInformation.Struct
2042 | SymbolDefinition.Const -> SymbolInformation.Constant
2043 | SymbolDefinition.Enum -> SymbolInformation.Enum
2044 | SymbolDefinition.Interface -> SymbolInformation.Interface
2045 | SymbolDefinition.Trait -> SymbolInformation.Interface
2046 (* LSP doesn't have traits, so we approximate with interface *)
2047 | SymbolDefinition.LocalVar -> SymbolInformation.Variable
2048 | SymbolDefinition.Typeconst -> SymbolInformation.Class
2049 (* e.g. "const type Ta = string;" -- absent from LSP *)
2050 | SymbolDefinition.Typedef -> SymbolInformation.Class
2051 (* e.g. top level type alias -- absent from LSP *)
2052 | SymbolDefinition.Param -> SymbolInformation.Variable
2053 (* We never return a param from a document-symbol-search *)
2055 let hack_symbol_to_lsp definition containerName =
2057 SymbolInformation.name = definition.name;
2058 kind = hack_to_lsp_kind definition.kind;
2059 location =
2060 hack_symbol_definition_to_lsp_construct_location
2061 definition
2062 ~default_path:filename;
2063 containerName;
2066 match defs with
2067 (* Flattens the recursive list of symbols *)
2068 | [] -> List.rev accu
2069 | def :: defs ->
2070 let children = Option.value def.children ~default:[] in
2071 let accu = hack_symbol_to_lsp def container_name :: accu in
2072 let accu =
2073 hack_symbol_tree_to_lsp
2074 ~filename
2075 ~accu
2076 ~container_name:(Some def.name)
2077 children
2079 hack_symbol_tree_to_lsp ~filename ~accu ~container_name defs
2081 let do_documentSymbol
2082 (conn : server_conn)
2083 (ref_unblocked_time : float ref)
2084 (params : DocumentSymbol.params) : DocumentSymbol.result Lwt.t =
2085 let open DocumentSymbol in
2086 let open TextDocumentIdentifier in
2087 let filename = lsp_uri_to_path params.textDocument.uri in
2088 let command = ServerCommandTypes.OUTLINE filename in
2089 let%lwt outline = rpc conn ref_unblocked_time command in
2090 let converted =
2091 hack_symbol_tree_to_lsp ~filename ~accu:[] ~container_name:None outline
2093 Lwt.return converted
2095 (* for serverless ide *)
2096 let do_documentSymbol_local
2097 (ide_service : ClientIdeService.t)
2098 (tracking_id : string)
2099 (ref_unblocked_time : float ref)
2100 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2101 (params : DocumentSymbol.params) : DocumentSymbol.result Lwt.t =
2102 let open DocumentSymbol in
2103 let open TextDocumentIdentifier in
2104 let filename = lsp_uri_to_path params.textDocument.uri in
2105 let document_location =
2107 ClientIdeMessage.file_path = Path.make filename;
2108 file_contents =
2109 get_document_contents editor_open_files params.textDocument.uri;
2110 line = 0;
2111 column = 0;
2114 let request = ClientIdeMessage.Document_symbol document_location in
2115 let%lwt results =
2116 ClientIdeService.rpc
2117 ide_service
2118 ~tracking_id
2119 ~ref_unblocked_time
2120 ~needs_init:true
2121 request
2123 match results with
2124 | Ok outline ->
2125 let converted =
2126 hack_symbol_tree_to_lsp ~filename ~accu:[] ~container_name:None outline
2128 Lwt.return converted
2129 | Error edata -> raise (Server_nonfatal_exception edata)
2131 let do_findReferences
2132 (conn : server_conn)
2133 (ref_unblocked_time : float ref)
2134 (params : FindReferences.params) : FindReferences.result Lwt.t =
2135 let { Ide_api_types.line; column } =
2136 lsp_position_to_ide
2137 params.FindReferences.loc.TextDocumentPositionParams.position
2139 let filename =
2140 Lsp_helpers.lsp_textDocumentIdentifier_to_filename
2141 params.FindReferences.loc.TextDocumentPositionParams.textDocument
2143 let include_defs =
2144 params.FindReferences.context.FindReferences.includeDeclaration
2146 let labelled_file = ServerCommandTypes.LabelledFileName filename in
2147 let command =
2148 ServerCommandTypes.IDE_FIND_REFS (labelled_file, line, column, include_defs)
2150 let%lwt results = rpc_with_retry conn ref_unblocked_time command in
2151 (* TODO: respect params.context.include_declaration *)
2152 match results with
2153 | None -> Lwt.return []
2154 | Some (_name, positions) ->
2155 Lwt.return
2156 (List.map positions ~f:(hack_pos_to_lsp_location ~default_path:filename))
2158 let do_goToImplementation
2159 (conn : server_conn)
2160 (ref_unblocked_time : float ref)
2161 (params : Implementation.params) : Implementation.result Lwt.t =
2162 let { Ide_api_types.line; column } =
2163 lsp_position_to_ide params.TextDocumentPositionParams.position
2165 let filename =
2166 Lsp_helpers.lsp_textDocumentIdentifier_to_filename
2167 params.TextDocumentPositionParams.textDocument
2169 let labelled_file = ServerCommandTypes.LabelledFileName filename in
2170 let command =
2171 ServerCommandTypes.IDE_GO_TO_IMPL (labelled_file, line, column)
2173 let%lwt results = rpc_with_retry conn ref_unblocked_time command in
2174 match results with
2175 | None -> Lwt.return []
2176 | Some (_name, positions) ->
2177 Lwt.return
2178 (List.map positions ~f:(hack_pos_to_lsp_location ~default_path:filename))
2180 (* Shared function for hack range conversion *)
2181 let hack_range_to_lsp_highlight range =
2182 { DocumentHighlight.range = ide_range_to_lsp range; kind = None }
2184 let do_documentHighlight
2185 (conn : server_conn)
2186 (ref_unblocked_time : float ref)
2187 (params : DocumentHighlight.params) : DocumentHighlight.result Lwt.t =
2188 let (file, line, column) = lsp_file_position_to_hack params in
2189 let command =
2190 ServerCommandTypes.(IDE_HIGHLIGHT_REFS (file, FileName file, line, column))
2192 let%lwt results = rpc conn ref_unblocked_time command in
2193 Lwt.return (List.map results ~f:hack_range_to_lsp_highlight)
2195 (* Serverless IDE implementation of highlight *)
2196 let do_highlight_local
2197 (ide_service : ClientIdeService.t)
2198 (tracking_id : string)
2199 (ref_unblocked_time : float ref)
2200 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2201 (params : DocumentHighlight.params) : DocumentHighlight.result Lwt.t =
2202 let document_location = get_document_location editor_open_files params in
2203 let%lwt result =
2204 ClientIdeService.rpc
2205 ide_service
2206 ~tracking_id
2207 ~ref_unblocked_time
2208 ~needs_init:true
2209 (ClientIdeMessage.Document_highlight document_location)
2211 match result with
2212 | Ok ranges -> Lwt.return (List.map ranges ~f:hack_range_to_lsp_highlight)
2213 | Error edata -> raise (Server_nonfatal_exception edata)
2215 let format_typeCoverage_result ~(equal : 'a -> 'a -> bool) results counts =
2216 TypeCoverageFB.(
2217 let coveredPercent = Coverage_level.get_percent counts in
2218 let hack_coverage_to_lsp (pos, level) =
2219 let range = hack_pos_to_lsp_range ~equal pos in
2220 match level with
2221 (* We only show diagnostics for completely untypechecked code. *)
2222 | Ide_api_types.Checked
2223 | Ide_api_types.Partial ->
2224 None
2225 | Ide_api_types.Unchecked -> Some { range; message = None }
2228 coveredPercent;
2229 uncoveredRanges = List.filter_map results ~f:hack_coverage_to_lsp;
2230 defaultMessage = "Un-type checked code. Consider adding type annotations.";
2233 let do_typeCoverageFB
2234 (conn : server_conn)
2235 (ref_unblocked_time : float ref)
2236 (params : TypeCoverageFB.params) : TypeCoverageFB.result Lwt.t =
2237 TypeCoverageFB.(
2238 let filename =
2239 Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument
2241 let command =
2242 ServerCommandTypes.COVERAGE_LEVELS
2243 (filename, ServerCommandTypes.FileName filename)
2245 let%lwt (results, counts) : Coverage_level_defs.result =
2246 rpc conn ref_unblocked_time command
2248 let formatted =
2249 format_typeCoverage_result ~equal:String.equal results counts
2251 Lwt.return formatted)
2253 let do_typeCoverage_localFB
2254 (ide_service : ClientIdeService.t)
2255 (tracking_id : string)
2256 (ref_unblocked_time : float ref)
2257 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2258 (params : TypeCoverageFB.params) : TypeCoverageFB.result Lwt.t =
2259 let open TypeCoverageFB in
2260 let document_contents =
2261 get_document_contents
2262 editor_open_files
2263 params.textDocument.TextDocumentIdentifier.uri
2265 match document_contents with
2266 | None -> failwith "Local type coverage failed, file could not be found."
2267 | Some file_contents ->
2268 let file_path =
2269 params.textDocument.TextDocumentIdentifier.uri
2270 |> lsp_uri_to_path
2271 |> Path.make
2273 let request =
2274 ClientIdeMessage.Type_coverage
2275 { ClientIdeMessage.file_path; ClientIdeMessage.file_contents }
2277 let%lwt result =
2278 ClientIdeService.rpc
2279 ide_service
2280 ~tracking_id
2281 ~ref_unblocked_time
2282 ~needs_init:true
2283 request
2285 (match result with
2286 | Ok (results, counts) ->
2287 let formatted =
2288 format_typeCoverage_result ~equal:String.equal results counts
2290 Lwt.return formatted
2291 | Error edata -> raise (Server_nonfatal_exception edata))
2293 let do_formatting_common
2294 (uri : Lsp.documentUri)
2295 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2296 (action : ServerFormatTypes.ide_action)
2297 (options : DocumentFormatting.formattingOptions) : TextEdit.t list =
2298 let open ServerFormatTypes in
2299 let filename_for_logging = lsp_uri_to_path uri in
2300 (* Following line will throw if the document isn't already open, so we'll *)
2301 (* return an error code to the LSP client. The spec doesn't spell out if we *)
2302 (* should be expected to handle formatting requests on unopened files. *)
2303 let lsp_doc = UriMap.find uri editor_open_files in
2304 let content = lsp_doc.Lsp.TextDocumentItem.text in
2305 let response =
2306 ServerFormat.go_ide ~filename_for_logging ~content ~action ~options
2308 match response with
2309 | Error "File failed to parse without errors" ->
2310 (* If LSP issues a formatting request at a given line+char, but we can't *)
2311 (* calculate a better format for the file due to syntax errors in it, *)
2312 (* then we should return "success and there are no edits to apply" *)
2313 (* rather than "error". *)
2314 (* TODO: let's eliminate hh_format, and incorporate hackfmt into the *)
2315 (* hh_client binary itself, and make make "hackfmt" just a wrapper for *)
2316 (* "hh_client format", and then make it return proper error that we can *)
2317 (* pattern-match upon, rather than hard-coding the string... *)
2319 | Error message ->
2320 raise
2321 (Error.LspException
2322 { Error.code = Error.UnknownErrorCode; message; data = None })
2323 | Ok r ->
2324 let range = ide_range_to_lsp r.range in
2325 let newText = r.new_text in
2326 [{ TextEdit.range; newText }]
2328 let do_documentRangeFormatting
2329 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2330 (params : DocumentRangeFormatting.params) : DocumentRangeFormatting.result =
2331 let open DocumentRangeFormatting in
2332 let open TextDocumentIdentifier in
2333 let action = ServerFormatTypes.Range (lsp_range_to_ide params.range) in
2334 do_formatting_common
2335 params.textDocument.uri
2336 editor_open_files
2337 action
2338 params.options
2340 let do_documentOnTypeFormatting
2341 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2342 (params : DocumentOnTypeFormatting.params) : DocumentOnTypeFormatting.result
2344 let open DocumentOnTypeFormatting in
2345 let open TextDocumentIdentifier in
2347 In LSP, positions do not point directly to characters, but to spaces in between characters.
2348 Thus, the LSP position that the cursor points to after typing a character is the space
2349 immediately after the character.
2351 For example:
2352 Character positions: 0 1 2 3 4 5 6
2353 f o o ( ) { }
2354 LSP positions: 0 1 2 3 4 5 6 7
2356 The cursor is at LSP position 7 after typing the "}" of "foo(){}"
2357 But the character position of "}" is 6.
2359 Nuclide currently sends positions according to LSP, but everything else in the server
2360 and in hack formatting assumes that positions point directly to characters.
2362 Thus, to send the position of the character itself for formatting,
2363 we must subtract one.
2365 let position =
2366 { params.position with character = params.position.character - 1 }
2368 let action = ServerFormatTypes.Position (lsp_position_to_ide position) in
2369 do_formatting_common
2370 params.textDocument.uri
2371 editor_open_files
2372 action
2373 params.options
2375 let do_documentFormatting
2376 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2377 (params : DocumentFormatting.params) : DocumentFormatting.result =
2378 let open DocumentFormatting in
2379 let open TextDocumentIdentifier in
2380 let action = ServerFormatTypes.Document in
2381 do_formatting_common
2382 params.textDocument.uri
2383 editor_open_files
2384 action
2385 params.options
2387 let do_signatureHelp
2388 (conn : server_conn)
2389 (ref_unblocked_time : float ref)
2390 (params : SignatureHelp.params) : SignatureHelp.result Lwt.t =
2391 let (file, line, column) = lsp_file_position_to_hack params in
2392 let command = ServerCommandTypes.IDE_SIGNATURE_HELP (file, line, column) in
2393 rpc conn ref_unblocked_time command
2395 (* Serverless IDE version of signature help *)
2396 let do_signatureHelp_local
2397 (ide_service : ClientIdeService.t)
2398 (tracking_id : string)
2399 (ref_unblocked_time : float ref)
2400 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t)
2401 (params : SignatureHelp.params) : SignatureHelp.result Lwt.t =
2402 let document_location = get_document_location editor_open_files params in
2403 let%lwt result =
2404 ClientIdeService.rpc
2405 ide_service
2406 ~tracking_id
2407 ~ref_unblocked_time
2408 ~needs_init:true
2409 (ClientIdeMessage.Signature_help document_location)
2411 match result with
2412 | Ok signatures -> Lwt.return signatures
2413 | Error edata -> raise (Server_nonfatal_exception edata)
2415 let patch_to_workspace_edit_change (patch : ServerRefactorTypes.patch) :
2416 string * TextEdit.t =
2417 let open ServerRefactorTypes in
2418 let open Pos in
2419 let text_edit =
2420 match patch with
2421 | Insert insert_patch
2422 | Replace insert_patch ->
2424 TextEdit.range =
2425 hack_pos_to_lsp_range ~equal:String.equal insert_patch.pos;
2426 newText = insert_patch.text;
2428 | Remove pos ->
2430 TextEdit.range = hack_pos_to_lsp_range ~equal:String.equal pos;
2431 newText = "";
2434 let uri =
2435 match patch with
2436 | Insert insert_patch
2437 | Replace insert_patch ->
2438 File_url.create (filename insert_patch.pos)
2439 | Remove pos -> File_url.create (filename pos)
2441 (uri, text_edit)
2443 let patches_to_workspace_edit (patches : ServerRefactorTypes.patch list) :
2444 WorkspaceEdit.t =
2445 let changes = List.map patches ~f:patch_to_workspace_edit_change in
2446 let changes =
2447 List.fold changes ~init:SMap.empty ~f:(fun acc (uri, text_edit) ->
2448 let current_edits = Option.value ~default:[] (SMap.find_opt uri acc) in
2449 let new_edits = text_edit :: current_edits in
2450 SMap.add uri new_edits acc)
2452 { WorkspaceEdit.changes }
2454 let do_documentRename
2455 (conn : server_conn)
2456 (ref_unblocked_time : float ref)
2457 (params : Rename.params) : WorkspaceEdit.t Lwt.t =
2458 let (filename, line, char) =
2459 lsp_file_position_to_hack (rename_params_to_document_position params)
2461 let open Rename in
2462 let new_name = params.newName in
2463 let command =
2464 ServerCommandTypes.IDE_REFACTOR
2465 { ServerCommandTypes.Ide_refactor_type.filename; line; char; new_name }
2467 let%lwt patches = rpc_with_retry conn ref_unblocked_time command in
2468 let patches =
2469 match patches with
2470 | Ok patches -> patches
2471 | Error message ->
2472 raise
2473 (Error.LspException
2474 { Error.code = Error.InvalidRequest; message; data = None })
2476 Lwt.return (patches_to_workspace_edit patches)
2478 (** This updates Main_env.hh_server_status according to the status message
2479 we just received from hh_server. See comments on hh_server_status for
2480 the invariants on its fields. *)
2481 let do_server_busy (state : state) (status : ServerCommandTypes.busy_status) :
2482 state =
2483 let open Main_env in
2484 let open ServerCommandTypes in
2485 let (type_, shortMessage, message) =
2486 match status with
2487 | Needs_local_typecheck ->
2488 (MessageType.InfoMessage, "Hack", "hh_server is preparing to check edits")
2489 | Doing_local_typecheck ->
2490 (MessageType.WarningMessage, "Hack", "hh_server is checking edits")
2491 | Done_local_typecheck ->
2492 ( MessageType.InfoMessage,
2493 "Hack",
2494 "hh_server is initialized and running correctly." )
2495 | Doing_global_typecheck Blocking ->
2496 ( MessageType.WarningMessage,
2497 "Hack: busy",
2498 "hh_server is typechecking the entire project (blocking)" )
2499 | Doing_global_typecheck Interruptible ->
2500 ( MessageType.WarningMessage,
2501 "Hack",
2502 "hh_server is typechecking entire project" )
2503 | Doing_global_typecheck (Remote_blocking message) ->
2504 ( MessageType.WarningMessage,
2505 "Hack: remote",
2506 "hh_server is remote-typechecking the entire project - " ^ message )
2507 | Done_global_typecheck _ ->
2508 ( MessageType.InfoMessage,
2509 "Hack",
2510 "hh_server is initialized and running correctly." )
2512 match state with
2513 | Main_loop menv ->
2514 let hh_server_status =
2516 ShowStatusFB.shortMessage = Some shortMessage;
2517 request = { ShowMessageRequest.type_; message; actions = [] };
2518 total = None;
2519 progress = None;
2522 Main_loop { menv with hh_server_status }
2523 | _ -> state
2525 (* do_diagnostics: sends notifications for all reported diagnostics; also *)
2526 (* returns an updated "uris_with_diagnostics" set of all files for which *)
2527 (* our client currently has non-empty diagnostic reports. *)
2528 let do_diagnostics
2529 (uris_with_diagnostics : UriSet.t)
2530 (file_reports : Pos.absolute Errors.error_ list SMap.t) : UriSet.t =
2531 (* Hack sometimes reports a diagnostic on an empty file when it can't *)
2532 (* figure out which file to report. In this case we'll report on the root. *)
2533 (* Nuclide and VSCode both display this fine, though they obviously don't *)
2534 (* let you click-to-go-to-file on it. *)
2535 let default_path = get_root_exn () |> Path.to_string in
2536 let file_reports =
2537 match SMap.find_opt "" file_reports with
2538 | None -> file_reports
2539 | Some errors ->
2540 SMap.remove "" file_reports |> SMap.add ~combine:( @ ) default_path errors
2542 let per_file file errors =
2543 let params = hack_errors_to_lsp_diagnostic file errors in
2544 let notification = PublishDiagnosticsNotification params in
2545 notify_jsonrpc ~powered_by:Hh_server notification
2547 SMap.iter per_file file_reports;
2549 let is_error_free _uri errors = List.is_empty errors in
2550 (* reports_without/reports_with are maps of filename->ErrorList. *)
2551 let (reports_without, reports_with) =
2552 SMap.partition is_error_free file_reports
2554 (* files_without/files_with are sets of filenames *)
2555 let files_without = SMap.bindings reports_without |> List.map ~f:fst in
2556 let files_with = SMap.bindings reports_with |> List.map ~f:fst in
2557 (* uris_without/uris_with are sets of uris *)
2558 let uris_without =
2559 List.map files_without ~f:(path_to_lsp_uri ~default_path) |> UriSet.of_list
2561 let uris_with =
2562 List.map files_with ~f:(path_to_lsp_uri ~default_path) |> UriSet.of_list
2564 (* this is "(uris_with_diagnostics \ uris_without) U uris_with" *)
2565 UriSet.union (UriSet.diff uris_with_diagnostics uris_without) uris_with
2567 let report_connect_end (ienv : In_init_env.t) : state =
2568 log "report_connect_end";
2569 In_init_env.(
2570 let _state = dismiss_diagnostics (In_init ienv) in
2571 let menv =
2573 Main_env.conn = ienv.In_init_env.conn;
2574 needs_idle = true;
2575 most_recent_file = ienv.most_recent_file;
2576 editor_open_files = ienv.editor_open_files;
2577 uris_with_diagnostics = UriSet.empty;
2578 uris_with_unsaved_changes = ienv.In_init_env.uris_with_unsaved_changes;
2579 hh_server_status =
2581 ShowStatusFB.request =
2583 ShowMessageRequest.type_ = MessageType.InfoMessage;
2584 message = "hh_server: ready.";
2585 actions = [];
2587 progress = None;
2588 total = None;
2589 shortMessage = None;
2593 Main_loop menv)
2595 (* After the server has sent 'hello', it means the persistent connection is *)
2596 (* ready, so we can send our backlog of file-edits to the server. *)
2597 let connect_after_hello (server_conn : server_conn) (state : state) : unit Lwt.t
2599 log "connect_after_hello";
2600 let ignore = ref 0.0 in
2601 let%lwt () =
2602 try%lwt
2603 (* tell server we want persistent connection *)
2604 let oc = server_conn.oc in
2605 ServerCommandLwt.send_connection_type oc ServerCommandTypes.Persistent;
2606 let fd = oc |> Unix.descr_of_out_channel |> Lwt_unix.of_unix_file_descr in
2607 let%lwt (response : 'a ServerCommandTypes.message_type) =
2608 Marshal_tools_lwt.from_fd_with_preamble fd
2610 begin
2611 match response with
2612 | ServerCommandTypes.Response (ServerCommandTypes.Connected, _) ->
2613 set_hh_server_state Hh_server_handling_or_ready
2614 | _ -> failwith "Didn't get server Connected response"
2615 end;
2617 (* tell server we want diagnostics *)
2618 log "Diag_subscribe: clientLsp subscribing diagnostic 0";
2619 let%lwt () =
2620 rpc server_conn ignore (ServerCommandTypes.SUBSCRIBE_DIAGNOSTIC 0)
2622 (* Extract the list of file changes we're tracking *)
2623 let editor_open_files =
2624 UriMap.elements
2625 (match state with
2626 | Main_loop menv -> Main_env.(menv.editor_open_files)
2627 | In_init ienv -> In_init_env.(ienv.editor_open_files)
2628 | Lost_server lenv -> Lost_env.(lenv.editor_open_files)
2629 | _ -> UriMap.empty)
2631 (* send open files and unsaved buffers to server *)
2632 let float_unblocked_time = ref 0.0 in
2633 (* Note: do serially since these involve RPC calls. *)
2634 let%lwt () =
2635 Lwt_list.iter_s
2636 (fun (uri, textDocument) ->
2637 let filename = lsp_uri_to_path uri in
2638 let command =
2639 ServerCommandTypes.OPEN_FILE
2640 (filename, textDocument.TextDocumentItem.text)
2642 rpc server_conn float_unblocked_time command)
2643 editor_open_files
2645 Lwt.return_unit
2646 with e ->
2647 let message = Exn.to_string e in
2648 let stack = Printexc.get_backtrace () in
2649 log "connect_after_hello exception %s\n%s" message stack;
2650 raise (Server_fatal_connection_exception { Marshal_tools.message; stack })
2652 Lwt.return_unit
2654 let rec connect_client ~(env : env) (root : Path.t) ~(autostart : bool) :
2655 server_conn Lwt.t =
2656 log "connect_client";
2657 Exit_status.(
2658 (* This basically does the same connection attempt as "hh_client check": *)
2659 (* it makes repeated attempts to connect; it prints useful messages to *)
2660 (* stderr; in case of failure it will raise an exception. Below we're *)
2661 (* catching the main exceptions so we can give a good user-facing error *)
2662 (* text. For other exceptions, they'll end up showing to the user just *)
2663 (* "internal error" with the error code. *)
2664 let env_connect =
2666 ClientConnect.root;
2667 from = !from;
2668 autostart;
2669 force_dormant_start = false;
2670 watchman_debug_logging = false;
2671 (* If you want this, start the server manually in terminal. *)
2672 deadline = Some (Unix.time () +. 3.);
2673 (* limit to 3 seconds *)
2674 no_load = false;
2675 (* only relevant when autostart=true *)
2676 log_inference_constraints = false;
2677 (* irrelevant *)
2678 profile_log = false;
2679 (* irrelevant *)
2680 remote = false;
2681 (* irrelevant *)
2682 ai_mode = None;
2683 (* only relevant when autostart=true *)
2684 progress_callback = ClientConnect.null_progress_reporter;
2685 (* we're fast! *)
2686 do_post_handoff_handshake = false;
2687 ignore_hh_version = false;
2688 saved_state_ignore_hhconfig = false;
2689 (* priority_pipe delivers good experience for hh_server, but has a bug,
2690 and doesn't provide benefits in serverless-ide. *)
2691 use_priority_pipe = not env.use_serverless_ide;
2692 prechecked = None;
2693 config = env.config;
2694 allow_non_opt_build = false;
2697 try%lwt
2698 let%lwt ClientConnect.{ channels = (ic, oc); server_finale_file; _ } =
2699 ClientConnect.connect env_connect
2701 can_autostart_after_mismatch := false;
2702 let pending_messages = Queue.create () in
2703 Lwt.return { ic; oc; pending_messages; server_finale_file }
2704 with Exit_with Build_id_mismatch when !can_autostart_after_mismatch ->
2705 (* Raised when the server was running an old version. We'll retry once. *)
2706 log "connect_client: build_id_mismatch";
2707 can_autostart_after_mismatch := false;
2708 connect_client ~env root ~autostart:true)
2710 let do_initialize ~(env : env) (root : Path.t) : Initialize.result =
2711 let server_args = ServerArgs.default_options ~root:(Path.to_string root) in
2712 let server_args = ServerArgs.set_config server_args env.config in
2713 let server_local_config =
2714 snd @@ ServerConfig.load ~silent:true ServerConfig.filename server_args
2716 Initialize.
2718 server_capabilities =
2720 textDocumentSync =
2722 want_openClose = true;
2723 want_change = IncrementalSync;
2724 want_willSave = false;
2725 want_willSaveWaitUntil = false;
2726 want_didSave = Some { includeText = false };
2728 hoverProvider = true;
2729 completionProvider =
2730 Some
2732 resolveProvider = true;
2733 completion_triggerCharacters =
2734 ["$"; ">"; "\\"; ":"; "<"; "["; "'"; "\""];
2736 signatureHelpProvider =
2737 Some { sighelp_triggerCharacters = ["("; ","] };
2738 definitionProvider = true;
2739 typeDefinitionProvider = true;
2740 referencesProvider = true;
2741 documentHighlightProvider = true;
2742 documentSymbolProvider = true;
2743 workspaceSymbolProvider = true;
2744 codeActionProvider = false;
2745 codeLensProvider = None;
2746 documentFormattingProvider = true;
2747 documentRangeFormattingProvider = true;
2748 documentOnTypeFormattingProvider =
2749 Some { firstTriggerCharacter = ";"; moreTriggerCharacter = ["}"] };
2750 renameProvider = true;
2751 documentLinkProvider = None;
2752 executeCommandProvider = None;
2753 implementationProvider =
2754 server_local_config.ServerLocalConfig.go_to_implementation;
2755 typeCoverageProviderFB = true;
2756 rageProviderFB = true;
2760 let do_didChangeWatchedFiles_registerCapability () : Lsp.lsp_request =
2761 let registration_options =
2762 DidChangeWatchedFilesRegistrationOptions
2764 DidChangeWatchedFiles.watchers =
2767 DidChangeWatchedFiles.globPattern
2768 (* We could be more precise here, but some language clients (such as
2769 LanguageClient-neovim) don't currently support rich glob patterns.
2770 We'll do further filtering at a later stage. *) =
2771 "**";
2776 let registration =
2777 Lsp.RegisterCapability.make_registration registration_options
2779 Lsp.RegisterCapabilityRequest
2780 { RegisterCapability.registrations = [registration] }
2782 let set_up_hh_logger_for_client_lsp (root : Path.t) : unit =
2783 (* Log to a file on disk. Note that calls to `Hh_logger` will always write to
2784 `stderr`; this is in addition to that. *)
2785 let client_lsp_log_fn = ServerFiles.client_lsp_log root in
2786 begin
2787 try Sys.rename client_lsp_log_fn (client_lsp_log_fn ^ ".old")
2788 with _e -> ()
2789 end;
2790 Hh_logger.set_log
2791 client_lsp_log_fn
2792 (Out_channel.create client_lsp_log_fn ~append:true);
2793 log "Starting clientLsp at %s" client_lsp_log_fn
2795 let start_server ~(env : env) (root : Path.t) : unit =
2796 (* This basically does "hh_client start": a single attempt to open the *)
2797 (* socket, send+read version and compare for mismatch, send handoff and *)
2798 (* read response. It will print information to stderr. If the server is in *)
2799 (* an unresponsive or invalid state then it will kill the server. Next if *)
2800 (* necessary it tries to spawn the server and wait until the monitor is *)
2801 (* responsive enough to print "ready". It will do a hard program exit if *)
2802 (* there were spawn problems. *)
2803 let env_start =
2805 ClientStart.root;
2806 from = !from;
2807 no_load = false;
2808 watchman_debug_logging = false;
2809 log_inference_constraints = false;
2810 profile_log = false;
2811 ai_mode = None;
2812 silent = true;
2813 exit_on_failure = false;
2814 debug_port = None;
2815 ignore_hh_version = false;
2816 saved_state_ignore_hhconfig = false;
2817 dynamic_view = !cached_toggle_state;
2818 prechecked = None;
2819 config = env.config;
2820 allow_non_opt_build = false;
2823 let _exit_status = ClientStart.main env_start in
2826 (* connect: this method either connects to the monitor and leaves in an *)
2827 (* In_init state waiting for the server hello, or it fails to connect and *)
2828 (* leaves in a Lost_server state. You might call this from Pre_init or *)
2829 (* Lost_server states, obviously. But you can also call it from In_init state *)
2830 (* if you want to give up on the prior attempt at connection and try again. *)
2831 let rec connect ~(env : env) (state : state) : state Lwt.t =
2832 begin
2833 match state with
2834 | In_init { In_init_env.conn; _ } ->
2835 begin
2837 Timeout.shutdown_connection conn.ic;
2838 Timeout.close_in_noerr conn.ic
2839 with _ -> ()
2841 | Pre_init
2842 | Lost_server _ ->
2844 | _ -> failwith "connect only in Pre_init, In_init or Lost_server state"
2845 end;
2846 try%lwt
2847 let%lwt conn = connect_client ~env (get_root_exn ()) ~autostart:false in
2848 set_hh_server_state Hh_server_initializing;
2849 match state with
2850 | In_init ienv ->
2851 Lwt.return
2852 (In_init
2853 { ienv with In_init_env.conn; most_recent_start_time = Unix.time () })
2854 | _ ->
2855 let state = dismiss_diagnostics state in
2856 Lwt.return
2857 (In_init
2859 In_init_env.conn;
2860 first_start_time = Unix.time ();
2861 most_recent_start_time = Unix.time ();
2862 most_recent_file = get_most_recent_file state;
2863 editor_open_files =
2864 Option.value (get_editor_open_files state) ~default:UriMap.empty;
2865 (* uris_with_unsaved_changes should always be empty here: *)
2866 (* Pre_init will of course be empty; *)
2867 (* Lost_server will exit rather than reconnect with unsaved changes. *)
2868 uris_with_unsaved_changes = get_uris_with_unsaved_changes state;
2869 hh_server_status_diagnostic = None;
2871 with e ->
2872 (* Exit_with Out_of_retries, Exit_with Out_of_time: raised when we *)
2873 (* couldn't complete the handshake up to handoff within 3 attempts over *)
2874 (* 3 seconds. Maybe the informant is stopping anything from happening *)
2875 (* until a rebase has settled? *)
2876 (* Exit_with No_server_running: raised when (1) the server's simply not *)
2877 (* running, or there's some other reason why the connection was refused *)
2878 (* or timed-out and no lockfile is present; (2) the server was dormant *)
2879 (* and had already received too many pending connection requests; *)
2880 (* (3) server failed to load saved-state but was required to do so. *)
2881 (* Exit_with Monitor_connection_failure: raised when the lockfile is *)
2882 (* present but connection-attempt to the monitor times out - maybe it's *)
2883 (* under DDOS, or maybe it's declining to answer new connections. *)
2884 let stack = Printexc.get_backtrace () in
2885 let { Lsp.Error.code; message; _ } = Lsp_fmt.error_of_exn e in
2886 let longMessage =
2887 Printf.sprintf
2888 "connect failed: %s [%s]\n%s"
2889 message
2890 (Lsp.Error.show_code code)
2891 stack
2893 let () = Lsp_helpers.telemetry_error to_stdout longMessage in
2894 Exit_status.(
2895 let new_hh_server_state =
2896 match e with
2897 | Exit_with Build_id_mismatch
2898 | Exit_with No_server_running_should_retry
2899 | Exit_with Server_hung_up_should_retry
2900 | Exit_with Server_hung_up_should_abort ->
2901 Hh_server_stopped
2902 | Exit_with Out_of_retries
2903 | Exit_with Out_of_time ->
2904 Hh_server_denying_connection
2905 | _ -> Hh_server_unknown
2907 let explanation =
2908 match e with
2909 | Exit_with Out_of_retries
2910 | Exit_with Out_of_time ->
2911 "hh_server is waiting for things to settle"
2912 | Exit_with No_server_running_should_retry -> "hh_server: stopped."
2913 | _ -> "hh_server: " ^ message
2915 let%lwt state =
2916 do_lost_server
2917 state
2918 ~allow_immediate_reconnect:false
2919 ~env
2921 Lost_env.explanation;
2922 new_hh_server_state;
2923 start_on_click = true;
2924 trigger_on_lock_file = true;
2925 trigger_on_lsp = false;
2928 Lwt.return state)
2930 and reconnect_from_lost_if_necessary
2931 ~(env : env) (state : state) (reason : [> `Event of event | `Force_regain ])
2932 : state Lwt.t =
2933 Lost_env.(
2934 let should_reconnect =
2935 match (state, reason) with
2936 | (Lost_server _, `Force_regain) -> true
2937 | ( Lost_server { p = { trigger_on_lsp = true; _ }; _ },
2938 `Event
2939 (Client_message (_, (RequestMessage _ | NotificationMessage _))) )
2941 true
2942 | ( Lost_server { p = { trigger_on_lock_file = true; _ }; lock_file; _ },
2943 `Event Tick ) ->
2944 MonitorConnection.server_exists lock_file
2945 | (_, _) -> false
2947 if should_reconnect then
2948 let%lwt current_version = read_hhconfig_version () in
2949 let needs_to_terminate =
2950 not (String.equal !hhconfig_version current_version)
2952 if needs_to_terminate then (
2953 (* In these cases we have to terminate our LSP server, and trust the *)
2954 (* client to restart us. Note that we can't do clientStart because that *)
2955 (* would start our (old) version of hh_server, not the new one! *)
2956 let unsaved = get_uris_with_unsaved_changes state |> UriSet.elements in
2957 let unsaved_str =
2958 if List.is_empty unsaved then
2959 "[None]"
2960 else
2961 unsaved |> List.map ~f:string_of_uri |> String.concat ~sep:"\n"
2963 let message =
2964 "Unsaved files:\n"
2965 ^ unsaved_str
2966 ^ "\nVersion in hhconfig that spawned the current hh_client: "
2967 ^ !hhconfig_version
2968 ^ "\nVersion in hhconfig currently: "
2969 ^ current_version
2970 ^ "\n"
2972 Lsp_helpers.telemetry_log to_stdout message;
2973 exit_fail ()
2974 ) else
2975 let%lwt state = connect ~env state in
2976 Lwt.return state
2977 else
2978 Lwt.return state)
2980 (* do_lost_server: handles the various ways we might lose hh_server. We keep *)
2981 (* the LSP server alive, and will (elsewhere) listen for the various triggers *)
2982 (* of getting the server back. *)
2983 and do_lost_server
2984 (state : state)
2985 ~(env : env)
2986 ?(allow_immediate_reconnect = true)
2987 (p : Lost_env.params) : state Lwt.t =
2988 Lost_env.(
2989 set_hh_server_state p.new_hh_server_state;
2991 let state = dismiss_diagnostics state in
2992 let uris_with_unsaved_changes = get_uris_with_unsaved_changes state in
2993 let most_recent_file = get_most_recent_file state in
2994 let editor_open_files =
2995 Option.value (get_editor_open_files state) ~default:UriMap.empty
2997 let lock_file = ServerFiles.lock_file (get_root_exn ()) in
2998 let reconnect_immediately =
2999 allow_immediate_reconnect
3000 && p.trigger_on_lock_file
3001 && MonitorConnection.server_exists lock_file
3003 if reconnect_immediately then (
3004 let lost_state =
3005 Lost_server
3007 Lost_env.p;
3008 most_recent_file;
3009 editor_open_files;
3010 uris_with_unsaved_changes;
3011 lock_file;
3012 hh_server_status_diagnostic = None;
3015 Lsp_helpers.telemetry_log
3016 to_stdout
3017 "Reconnecting immediately to hh_server";
3018 let%lwt new_state =
3019 reconnect_from_lost_if_necessary ~env lost_state `Force_regain
3021 Lwt.return new_state
3022 ) else
3023 Lwt.return
3024 (Lost_server
3026 Lost_env.p;
3027 most_recent_file;
3028 editor_open_files;
3029 uris_with_unsaved_changes;
3030 lock_file;
3031 hh_server_status_diagnostic = None;
3034 let handle_idle_if_necessary (state : state) (event : event) : state =
3035 match state with
3036 | Main_loop menv when not (is_tick event) ->
3037 Main_loop { menv with Main_env.needs_idle = true }
3038 | _ -> state
3040 let track_open_and_recent_files (state : state) (event : event) : state =
3041 (* We'll keep track of which files are opened by the editor. *)
3042 let prev_opened_files =
3043 Option.value (get_editor_open_files state) ~default:UriMap.empty
3045 let editor_open_files =
3046 match event with
3047 | Client_message (_, NotificationMessage (DidOpenNotification params)) ->
3048 let doc = params.DidOpen.textDocument in
3049 let uri = params.DidOpen.textDocument.TextDocumentItem.uri in
3050 UriMap.add uri doc prev_opened_files
3051 | Client_message (_, NotificationMessage (DidChangeNotification params)) ->
3052 let uri =
3053 params.DidChange.textDocument.VersionedTextDocumentIdentifier.uri
3055 let doc = UriMap.find_opt uri prev_opened_files in
3056 let open Lsp.TextDocumentItem in
3057 (match doc with
3058 | Some doc ->
3059 let doc' =
3061 doc with
3062 version =
3063 params.DidChange.textDocument
3064 .VersionedTextDocumentIdentifier.version;
3065 text =
3066 Lsp_helpers.apply_changes_unsafe
3067 doc.text
3068 params.DidChange.contentChanges;
3071 UriMap.add uri doc' prev_opened_files
3072 | None -> prev_opened_files)
3073 | Client_message (_, NotificationMessage (DidCloseNotification params)) ->
3074 let uri = params.DidClose.textDocument.TextDocumentIdentifier.uri in
3075 UriMap.remove uri prev_opened_files
3076 | _ -> prev_opened_files
3078 (* We'll track which was the most recent file to have an event *)
3079 let most_recent_file =
3080 match event with
3081 | Client_message (_metadata, message) ->
3082 let uri = Lsp_fmt.get_uri_opt message in
3083 if Option.is_some uri then
3085 else
3086 get_most_recent_file state
3087 | _ -> get_most_recent_file state
3089 match state with
3090 | Main_loop menv ->
3091 Main_loop { menv with Main_env.editor_open_files; most_recent_file }
3092 | In_init ienv ->
3093 In_init { ienv with In_init_env.editor_open_files; most_recent_file }
3094 | Lost_server lenv ->
3095 Lost_server { lenv with Lost_env.editor_open_files; most_recent_file }
3096 | _ -> state
3098 let track_edits_if_necessary (state : state) (event : event) : state =
3099 (* We'll keep track of which files have unsaved edits. Note that not all *)
3100 (* clients send didSave messages; for those we only rely on didClose. *)
3101 let previous = get_uris_with_unsaved_changes state in
3102 let uris_with_unsaved_changes =
3103 match event with
3104 | Client_message (_, NotificationMessage (DidChangeNotification params)) ->
3105 let uri =
3106 params.DidChange.textDocument.VersionedTextDocumentIdentifier.uri
3108 UriSet.add uri previous
3109 | Client_message (_, NotificationMessage (DidCloseNotification params)) ->
3110 let uri = params.DidClose.textDocument.TextDocumentIdentifier.uri in
3111 UriSet.remove uri previous
3112 | Client_message (_, NotificationMessage (DidSaveNotification params)) ->
3113 let uri = params.DidSave.textDocument.TextDocumentIdentifier.uri in
3114 UriSet.remove uri previous
3115 | _ -> previous
3117 match state with
3118 | Main_loop menv -> Main_loop { menv with Main_env.uris_with_unsaved_changes }
3119 | In_init ienv -> In_init { ienv with In_init_env.uris_with_unsaved_changes }
3120 | Lost_server lenv ->
3121 Lost_server { lenv with Lost_env.uris_with_unsaved_changes }
3122 | _ -> state
3124 let get_filename_in_message_for_logging (message : lsp_message) :
3125 Relative_path.t option =
3126 let uri_opt = Lsp_fmt.get_uri_opt message in
3127 match uri_opt with
3128 | None -> None
3129 | Some uri ->
3130 (try
3131 let path = Lsp_helpers.lsp_uri_to_path uri in
3132 Some (Relative_path.create_detect_prefix path)
3133 with _ ->
3134 Some (Relative_path.create Relative_path.Dummy (Lsp.string_of_uri uri)))
3136 (* Historical quirk: we log kind and method-name a bit idiosyncratically... *)
3137 let get_message_kind_and_method_for_logging (message : lsp_message) :
3138 string * string =
3139 match message with
3140 | ResponseMessage (_, _) -> ("Response", "[response]")
3141 | RequestMessage (_, r) -> ("Request", Lsp_fmt.request_name_to_string r)
3142 | NotificationMessage n ->
3143 ("Notification", Lsp_fmt.notification_name_to_string n)
3145 let log_response_if_necessary
3146 (env : env)
3147 (event : event)
3148 (result_telemetry_opt : result_telemetry option)
3149 (unblocked_time : float) : unit =
3150 match event with
3151 | Client_message (metadata, message) ->
3152 let (kind, method_) = get_message_kind_and_method_for_logging message in
3153 let t = Unix.gettimeofday () in
3154 log_debug
3155 "lsp-message [%s] queue time [%0.3f] execution time [%0.3f"
3156 method_
3157 (unblocked_time -. metadata.timestamp)
3158 (t -. unblocked_time);
3159 let (result_count, result_extra_telemetry) =
3160 match result_telemetry_opt with
3161 | None -> (None, None)
3162 | Some { result_count; result_extra_telemetry } ->
3163 (Some result_count, result_extra_telemetry)
3165 HackEventLogger.client_lsp_method_handled
3166 ~root:(get_root_opt ())
3167 ~method_
3168 ~kind
3169 ~path_opt:(get_filename_in_message_for_logging message)
3170 ~result_count
3171 ~result_extra_telemetry
3172 ~tracking_id:metadata.tracking_id
3173 ~start_queue_time:metadata.timestamp
3174 ~start_hh_server_state:
3175 ( get_older_hh_server_state metadata.timestamp
3176 |> hh_server_state_to_string )
3177 ~start_handle_time:unblocked_time
3178 ~serverless_ide_flag:env.use_serverless_ide
3179 | _ -> ()
3181 type error_source =
3182 | Error_from_server_fatal
3183 | Error_from_client_fatal
3184 | Error_from_client_recoverable
3185 | Error_from_server_recoverable
3186 | Error_from_lsp_cancelled
3187 | Error_from_lsp_misc
3189 let hack_log_error
3190 (event : event option)
3191 (e : Lsp.Error.t)
3192 (source : error_source)
3193 (unblocked_time : float)
3194 (env : env) : unit =
3195 let root = get_root_opt () in
3196 let is_expected =
3197 match source with
3198 | Error_from_lsp_cancelled -> true
3199 | Error_from_server_fatal
3200 | Error_from_client_fatal
3201 | Error_from_client_recoverable
3202 | Error_from_server_recoverable
3203 | Error_from_lsp_misc ->
3204 false
3206 let source =
3207 match source with
3208 | Error_from_server_fatal -> "server_fatal"
3209 | Error_from_client_fatal -> "client_fatal"
3210 | Error_from_client_recoverable -> "client_recoverable"
3211 | Error_from_server_recoverable -> "server_recoverable"
3212 | Error_from_lsp_cancelled -> "lsp_cancelled"
3213 | Error_from_lsp_misc -> "lsp_misc"
3215 if not is_expected then log "%s" (Lsp_fmt.error_to_log_string e);
3216 match event with
3217 | Some (Client_message (metadata, message)) ->
3218 let start_hh_server_state =
3219 get_older_hh_server_state metadata.timestamp |> hh_server_state_to_string
3221 let (kind, method_) = get_message_kind_and_method_for_logging message in
3222 HackEventLogger.client_lsp_method_exception
3223 ~root
3224 ~method_
3225 ~kind
3226 ~path_opt:(get_filename_in_message_for_logging message)
3227 ~tracking_id:metadata.tracking_id
3228 ~start_queue_time:metadata.timestamp
3229 ~start_hh_server_state
3230 ~start_handle_time:unblocked_time
3231 ~serverless_ide_flag:env.use_serverless_ide
3232 ~message:e.Error.message
3233 ~data_opt:e.Error.data
3234 ~source
3235 | _ ->
3236 HackEventLogger.client_lsp_exception
3237 ~root
3238 ~message:e.Error.message
3239 ~data_opt:e.Error.data
3240 ~source
3242 (* cancel_if_stale: If a message is stale, throw the necessary exception to
3243 cancel it. A message is considered stale if it's sufficiently old and there
3244 are other messages in the queue that are newer than it. *)
3245 let short_timeout = 2.5
3247 let long_timeout = 15.0
3249 let cancel_if_stale
3250 (client : Jsonrpc.queue) (timestamp : float) (timeout : float) : unit Lwt.t
3252 let time_elapsed = Unix.gettimeofday () -. timestamp in
3253 if time_elapsed >= timeout then
3254 if Jsonrpc.has_message client then
3255 raise
3256 (Error.LspException
3258 Error.code = Error.RequestCancelled;
3259 message = "request timed out";
3260 data = None;
3262 else
3263 Lwt.return_unit
3264 else
3265 Lwt.return_unit
3267 (** Like all async methods, this method has a synchronous preamble up
3268 to its first await point, at which point it returns a promise to its
3269 caller; the rest of the method will be scheduled asynchronously.
3270 The synchrpnous preamble sends an "initialize" request to the ide_service.
3271 The asynchronous continuation is triggered when the response comes back;
3272 it then pumps messages to and from the ide service.
3273 Note: the fact that the request is sent in the synchronous preamble, is
3274 important for correctness - the rest of the codebase can send other requests
3275 to the ide_service at any time, safe in the knowledge that such requests will
3276 necessarily be delivered after the initialize request. *)
3277 let run_ide_service
3278 (env : env)
3279 (ide_service : ClientIdeService.t)
3280 (initialize_params : Lsp.Initialize.params)
3281 (editor_open_files : Lsp.TextDocumentItem.t UriMap.t option) : unit Lwt.t =
3282 let root = Some (Lsp_helpers.get_root initialize_params) |> Wwwroot.get in
3284 Lsp.Initialize.(
3285 initialize_params.client_capabilities.workspace.didChangeWatchedFiles
3286 .dynamicRegistration)
3287 then
3288 log "Language client reports that it supports file-watching"
3289 else
3291 ( "Warning: the language client does not report "
3292 ^^ "that it supports file-watching; "
3293 ^^ "file change notifications may not be processed, "
3294 ^^ "and consequently, IDE queries may return stale results." );
3296 let naming_table_saved_state_path =
3297 Lsp.Initialize.(
3298 initialize_params.initializationOptions.namingTableSavedStatePath)
3299 |> Option.map ~f:Path.make
3301 let open_files =
3302 editor_open_files
3303 |> Option.value ~default:UriMap.empty
3304 |> UriMap.keys
3305 |> List.map ~f:(fun uri -> uri |> lsp_uri_to_path |> Path.make)
3307 let%lwt result =
3308 ClientIdeService.initialize_from_saved_state
3309 ide_service
3310 ~root
3311 ~naming_table_saved_state_path
3312 ~wait_for_initialization:(Option.is_some naming_table_saved_state_path)
3313 ~use_ranked_autocomplete:env.use_ranked_autocomplete
3314 ~config:env.config
3315 ~open_files
3317 match result with
3318 | Ok num_changed_files_to_process ->
3319 Lsp_helpers.telemetry_log
3320 to_stdout
3321 (Printf.sprintf
3322 "[client-ide] Initialized; %d file changes to process"
3323 num_changed_files_to_process);
3324 let%lwt () = ClientIdeService.serve ide_service in
3325 Lwt.return_unit
3326 | Error
3328 ClientIdeMessage.medium_user_message;
3329 long_user_message;
3330 debug_details;
3331 is_actionable;
3333 } ->
3334 let input = Printf.sprintf "%s\n\n%s" long_user_message debug_details in
3335 let%lwt upload_result = Clowder_paste.clowder_paste ~timeout:10. input in
3336 let append_to_log =
3337 match upload_result with
3338 | Ok url -> Printf.sprintf "\nMore details: %s" url
3339 | Error message ->
3340 Printf.sprintf
3341 "\n\nMore details:\n%s\n\nTried to upload those details but it didn't work...\n%s"
3342 debug_details
3343 message
3346 "IDE services could not be initialized.\n%s\n%s"
3347 long_user_message
3348 debug_details;
3349 Lsp_helpers.log_error to_stdout (long_user_message ^ append_to_log);
3350 if is_actionable then
3351 Lsp_helpers.showMessage_error
3352 to_stdout
3353 (medium_user_message ^ see_output_hack);
3354 (* chevron *)
3355 Lwt.return_unit
3357 let on_status_restart_action
3358 ~(env : env)
3359 ~(init_id : string)
3360 ~(ide_service : ClientIdeService.t option ref)
3361 (result : ShowStatusFB.result)
3362 (state : state) : state Lwt.t =
3363 let open ShowMessageRequest in
3364 match (result, state, !ide_service) with
3365 | (Some { title }, Lost_server _, _)
3366 when String.equal title hh_server_restart_button_text ->
3367 let root = get_root_exn () in
3368 (* Belt-and-braces kill the server. This is in case the server was *)
3369 (* stuck in some weird state. It's also what 'hh restart' does. *)
3370 if MonitorConnection.server_exists (Path.to_string root) then
3371 ClientStop.kill_server root !from;
3373 (* After that it's safe to try to reconnect! *)
3374 start_server ~env root;
3375 let%lwt state = reconnect_from_lost_if_necessary ~env state `Force_regain in
3376 Lwt.return state
3377 | (Some { title }, _, Some old_ide_service)
3378 when String.equal title client_ide_restart_button_text ->
3379 log "Restarting IDE service";
3381 (* It's possible that [destroy] takes a while to finish, so make
3382 sure to assign the new IDE service to the [ref] before attempting
3383 to do an asynchronous operation with the old one. *)
3384 let ide_args = { ClientIdeMessage.init_id; verbose = env.verbose } in
3385 let new_ide_service = ClientIdeService.make ide_args in
3386 ide_service := Some new_ide_service;
3387 set_verbose_to_file
3388 ~ide_service:(Some new_ide_service)
3389 ~tracking_id:"[restart]"
3390 !verbose_to_file;
3391 (* Note: the env.verbose passed on init controls verbosity for stderr
3392 and is only ever controlled by --verbose command line, stored in env.
3393 But verbosity-to-file can be altered dynamically by the user. *)
3394 Lwt.async (fun () ->
3395 run_ide_service
3397 new_ide_service
3398 (initialize_params_exc ())
3399 (get_editor_open_files state));
3400 (* Invariant: at all times after InitializeRequest, ide_service has
3401 already been sent an "initialize" message. *)
3402 let%lwt () =
3403 stop_ide_service
3404 old_ide_service
3405 ~tracking_id:"restart"
3406 ~reason:ClientIdeService.Stop_reason.Restarting
3408 Lwt.return state
3409 | _ -> Lwt.return state
3411 (************************************************************************)
3412 (* Message handling *)
3413 (************************************************************************)
3415 (** send DidOpen/Close/Change/Save to hh_server and ide_service as needed *)
3416 let handle_editor_buffer_message
3417 ~(state : state)
3418 ~(ide_service : ClientIdeService.t option)
3419 ~(metadata : incoming_metadata)
3420 ~(ref_unblocked_time : float ref)
3421 ~(message : lsp_message) : unit Lwt.t =
3422 let uri_to_path uri = uri |> lsp_uri_to_path |> Path.make in
3423 let ref_hh_unblocked_time = ref 0. in
3424 let ref_ide_unblocked_time = ref 0. in
3426 (* send to hh_server as necessary *)
3427 let (hh_server_promise : unit Lwt.t) =
3428 let open Main_env in
3429 match (state, message) with
3430 (* textDocument/didOpen notification *)
3431 | (Main_loop menv, NotificationMessage (DidOpenNotification params)) ->
3432 let%lwt () = do_didOpen menv.conn ref_hh_unblocked_time params in
3433 Lwt.return_unit
3434 (* textDocument/didClose notification *)
3435 | (Main_loop menv, NotificationMessage (DidCloseNotification params)) ->
3436 let%lwt () = do_didClose menv.conn ref_hh_unblocked_time params in
3437 Lwt.return_unit
3438 (* textDocument/didChange notification *)
3439 | (Main_loop menv, NotificationMessage (DidChangeNotification params)) ->
3440 let%lwt () = do_didChange menv.conn ref_hh_unblocked_time params in
3441 Lwt.return_unit
3442 (* textDocument/didSave notification *)
3443 | (Main_loop _menv, NotificationMessage (DidSaveNotification _params)) ->
3444 Lwt.return_unit
3445 | (_, _) -> Lwt.return_unit
3448 (* send to ide_service as necessary *)
3449 (* For now 'ide_service_promise' is immediately fulfilled, but in future it will
3450 be fulfilled only when the ide_service has finished processing the message. *)
3451 let (ide_service_promise : unit Lwt.t) =
3452 match (ide_service, message) with
3453 | (Some ide_service, NotificationMessage (DidOpenNotification params)) ->
3454 let file_path =
3455 uri_to_path params.DidOpen.textDocument.TextDocumentItem.uri
3457 let file_contents = params.DidOpen.textDocument.TextDocumentItem.text in
3458 (* The ClientIdeDaemon only delivers answers for open files, which is why it's vital
3459 never to let is miss a DidOpen. *)
3460 let%lwt () =
3461 ide_rpc
3462 ide_service
3463 ~tracking_id:metadata.tracking_id
3464 ~ref_unblocked_time:ref_ide_unblocked_time
3465 ~needs_init:false
3466 ClientIdeMessage.(Ide_file_opened { file_path; file_contents })
3468 Lwt.return_unit
3469 | (Some ide_service, NotificationMessage (DidChangeNotification params)) ->
3470 let file_path =
3471 uri_to_path
3472 params.DidChange.textDocument.VersionedTextDocumentIdentifier.uri
3474 let%lwt () =
3475 ide_rpc
3476 ide_service
3477 ~tracking_id:metadata.tracking_id
3478 ~ref_unblocked_time:ref_ide_unblocked_time
3479 ~needs_init:false
3480 ClientIdeMessage.(Ide_file_changed { Ide_file_changed.file_path })
3482 Lwt.return_unit
3483 | (Some ide_service, NotificationMessage (DidCloseNotification params)) ->
3484 let file_path =
3485 uri_to_path params.DidClose.textDocument.TextDocumentIdentifier.uri
3487 let%lwt () =
3488 ide_rpc
3489 ide_service
3490 ~tracking_id:metadata.tracking_id
3491 ~ref_unblocked_time:ref_ide_unblocked_time
3492 ~needs_init:false
3493 ClientIdeMessage.(Ide_file_closed file_path)
3495 Lwt.return_unit
3496 | _ ->
3497 (* Don't handle other events for now. When we show typechecking errors for
3498 the open file, we'll start handling them. *)
3499 Lwt.return_unit
3502 (* Our asynchrony deal is (1) we want to kick off notifications to
3503 hh_server and ide_service at the same time, (2) we want to wait until
3504 both are done, (3) an exception in one shouldn't jeapordize the other,
3505 (4) our failure model only allows us to record at most one exception
3506 so we'll pick one arbitrarily. *)
3507 let%lwt (hh_server_e : Exception.t option) =
3508 try%lwt
3509 let%lwt () = hh_server_promise in
3510 Lwt.return_none
3511 with e -> Lwt.return_some (Exception.wrap e)
3512 and (ide_service_e : Exception.t option) =
3513 try%lwt
3514 let%lwt () = ide_service_promise in
3515 Lwt.return_none
3516 with e -> Lwt.return_some (Exception.wrap e)
3518 ref_unblocked_time := max !ref_hh_unblocked_time !ref_ide_unblocked_time;
3519 match (hh_server_e, ide_service_e) with
3520 | (_, Some e)
3521 | (Some e, _) ->
3522 Exception.reraise e
3523 | _ -> Lwt.return_unit
3525 (* handle_event: Process and respond to a message, and update the LSP state
3526 machine accordingly. In case the message was a request, it returns the
3527 json it responded with, so the caller can log it. *)
3528 let handle_client_message
3529 ~(env : env)
3530 ~(state : state ref)
3531 ~(client : Jsonrpc.queue)
3532 ~(ide_service : ClientIdeService.t option)
3533 ~(metadata : incoming_metadata)
3534 ~(message : lsp_message)
3535 ~(ref_unblocked_time : float ref) : result_telemetry option Lwt.t =
3536 let open Main_env in
3537 let%lwt result_telemetry_opt =
3538 (* make sure to wrap any exceptions below in the promise *)
3539 let tracking_id = metadata.tracking_id in
3540 let timestamp = metadata.timestamp in
3541 let editor_open_files =
3542 match get_editor_open_files !state with
3543 | Some files -> files
3544 | None -> UriMap.empty
3546 match (!state, ide_service, message) with
3547 (* response *)
3548 | (_, _, ResponseMessage (id, response)) ->
3549 let (_, handler) = IdMap.find id !requests_outstanding in
3550 let%lwt new_state = handler response !state in
3551 state := new_state;
3552 Lwt.return_none
3553 (* shutdown request *)
3554 | (_, _, RequestMessage (id, ShutdownRequest)) ->
3555 let%lwt new_state =
3556 do_shutdown !state ide_service tracking_id ref_unblocked_time
3558 state := new_state;
3559 respond_jsonrpc ~powered_by:Language_server id ShutdownResult;
3560 Lwt.return_none
3561 (* cancel notification *)
3562 | (_, _, NotificationMessage (CancelRequestNotification _)) ->
3563 (* For now, we'll ignore it. *)
3564 Lwt.return_none
3565 (* exit notification *)
3566 | (_, _, NotificationMessage ExitNotification) ->
3567 if is_post_shutdown !state then
3568 exit_ok ()
3569 else
3570 exit_fail ()
3571 (* setTrace notification *)
3572 | (_, _, NotificationMessage (SetTraceNotification params)) ->
3573 let value =
3574 match params with
3575 | SetTraceNotification.Verbose -> true
3576 | SetTraceNotification.Off -> false
3578 set_verbose_to_file ~ide_service ~tracking_id value;
3579 Lwt.return_none
3580 (* test entrypoint: shutdown client_ide_service *)
3581 | ( _,
3582 Some ide_service,
3583 RequestMessage (id, HackTestShutdownServerlessRequestFB) ) ->
3584 let%lwt () =
3585 stop_ide_service
3586 ide_service
3587 ~tracking_id
3588 ~reason:ClientIdeService.Stop_reason.Testing
3590 respond_jsonrpc
3591 ~powered_by:Serverless_ide
3593 HackTestShutdownServerlessResultFB;
3594 Lwt.return_none
3595 (* test entrypoint: stop hh_server *)
3596 | (_, _, RequestMessage (id, HackTestStopServerRequestFB)) ->
3597 let root_folder =
3598 Path.make (Relative_path.path_of_prefix Relative_path.Root)
3600 ClientStop.kill_server root_folder !from;
3601 respond_jsonrpc ~powered_by:Serverless_ide id HackTestStopServerResultFB;
3602 Lwt.return_none
3603 (* test entrypoint: start hh_server *)
3604 | (_, _, RequestMessage (id, HackTestStartServerRequestFB)) ->
3605 let root_folder =
3606 Path.make (Relative_path.path_of_prefix Relative_path.Root)
3608 start_server ~env root_folder;
3609 respond_jsonrpc ~powered_by:Serverless_ide id HackTestStartServerResultFB;
3610 Lwt.return_none
3611 (* initialize request *)
3612 | (Pre_init, _, RequestMessage (id, InitializeRequest initialize_params)) ->
3613 let open Initialize in
3614 initialize_params_ref := Some initialize_params;
3615 let root = get_root_exn () in
3616 (* calculated from initialize_params_ref *)
3617 set_up_hh_logger_for_client_lsp root;
3618 (* Following is a hack. Atom incorrectly passes '--from vscode', rendering us
3619 unable to distinguish Atom from VSCode. But Atom is now frozen at vscode client
3620 v3.14. So by looking at the version, we can at least distinguish that it's old. *)
3622 (not
3623 initialize_params.client_capabilities.textDocument.declaration
3624 .declarationLinkSupport)
3625 && String.equal env.from "vscode"
3626 then begin
3627 from := "vscode_pre314";
3628 HackEventLogger.set_from !from
3629 end;
3631 let%lwt version = read_hhconfig_version () in
3632 hhconfig_version := version;
3633 HackEventLogger.set_hhconfig_version
3634 (Some (String_utils.lstrip !hhconfig_version "^"));
3635 let%lwt new_state = connect ~env !state in
3636 state := new_state;
3637 Relative_path.set_path_prefix Relative_path.Root root;
3638 (* If editor sent 'trace: on' then that will turn on verbose_to_file. But we won't turn off
3639 verbose here, since the command-line argument --verbose trumps initialization params. *)
3640 begin
3641 match initialize_params.Initialize.trace with
3642 | Initialize.Off -> ()
3643 | Initialize.Messages
3644 | Initialize.Verbose ->
3645 set_verbose_to_file ~ide_service ~tracking_id true
3646 end;
3647 let result = do_initialize ~env root in
3648 respond_jsonrpc ~powered_by:Language_server id (InitializeResult result);
3650 begin
3651 match ide_service with
3652 | None -> ()
3653 | Some ide_service ->
3654 Lwt.async (fun () ->
3655 run_ide_service env ide_service initialize_params None);
3656 (* Invariant: at all times after InitializeRequest, ide_service has
3657 already been sent an "initialize" message. *)
3658 let id = NumberId (Jsonrpc.get_next_request_id ()) in
3659 let request = do_didChangeWatchedFiles_registerCapability () in
3660 to_stdout (print_lsp_request id request);
3661 (* TODO: our handler should really handle an error response properly *)
3662 let handler _response state = Lwt.return state in
3663 requests_outstanding :=
3664 IdMap.add id (request, handler) !requests_outstanding
3665 end;
3667 if not @@ Sys_utils.is_test_mode () then
3668 Lsp_helpers.telemetry_log
3669 to_stdout
3670 ("Version in hhconfig=" ^ !hhconfig_version);
3671 Lwt.return_some { result_count = 0; result_extra_telemetry = None }
3672 (* any request/notification if we haven't yet initialized *)
3673 | (Pre_init, _, _) ->
3674 raise
3675 (Error.LspException
3677 Error.code = Error.ServerNotInitialized;
3678 message = "Server not yet initialized";
3679 data = None;
3681 | (Post_shutdown, _, _c) ->
3682 raise
3683 (Error.LspException
3685 Error.code = Error.InvalidRequest;
3686 message = "already received shutdown request";
3687 data = None;
3689 (* initialized notification *)
3690 | (_, _, NotificationMessage InitializedNotification) -> Lwt.return_none
3691 (* rage request *)
3692 | (_, _, RequestMessage (id, RageRequestFB)) ->
3693 let%lwt result = do_rageFB !state ref_unblocked_time in
3694 respond_jsonrpc ~powered_by:Language_server id (RageResultFB result);
3695 Lwt.return_some
3696 { result_count = List.length result; result_extra_telemetry = None }
3697 | ( _,
3698 Some ide_service,
3699 NotificationMessage (DidChangeWatchedFilesNotification notification) )
3701 let open DidChangeWatchedFiles in
3702 let changes =
3703 List.map notification.changes ~f:(fun change ->
3704 change.uri |> lsp_uri_to_path |> Path.make)
3706 let%lwt () =
3707 ide_rpc
3708 ide_service
3709 ~tracking_id
3710 ~ref_unblocked_time
3711 ~needs_init:false
3712 ClientIdeMessage.(Disk_files_changed changes)
3714 Lwt.return_none
3715 (* Text document completion: "AutoComplete!" *)
3716 | (_, Some ide_service, RequestMessage (id, CompletionRequest params)) ->
3717 let%lwt () = cancel_if_stale client timestamp short_timeout in
3718 let%lwt result =
3719 do_completion_local
3720 ide_service
3721 tracking_id
3722 ref_unblocked_time
3723 editor_open_files
3724 params
3726 respond_jsonrpc ~powered_by:Serverless_ide id (CompletionResult result);
3727 Lwt.return_some
3729 result_count = List.length result.Completion.items;
3730 result_extra_telemetry = None;
3732 (* Resolve documentation for a symbol: "Autocomplete Docblock!" *)
3733 | ( _,
3734 Some ide_service,
3735 RequestMessage (id, CompletionItemResolveRequest params) ) ->
3736 let%lwt () = cancel_if_stale client timestamp short_timeout in
3737 let%lwt result =
3738 do_resolve_local
3739 ide_service
3740 tracking_id
3741 ref_unblocked_time
3742 editor_open_files
3743 params
3745 respond_jsonrpc
3746 ~powered_by:Serverless_ide
3748 (CompletionItemResolveResult result);
3749 Lwt.return_some { result_count = 1; result_extra_telemetry = None }
3750 (* Document highlighting in serverless IDE *)
3751 | (_, Some ide_service, RequestMessage (id, DocumentHighlightRequest params))
3753 let%lwt () = cancel_if_stale client timestamp short_timeout in
3754 let%lwt result =
3755 do_highlight_local
3756 ide_service
3757 tracking_id
3758 ref_unblocked_time
3759 editor_open_files
3760 params
3762 respond_jsonrpc
3763 ~powered_by:Serverless_ide
3765 (DocumentHighlightResult result);
3766 Lwt.return_some
3767 { result_count = List.length result; result_extra_telemetry = None }
3768 (* Type coverage in serverless IDE *)
3769 | (_, Some ide_service, RequestMessage (id, TypeCoverageRequestFB params))
3771 let%lwt () = cancel_if_stale client timestamp short_timeout in
3772 let%lwt result =
3773 do_typeCoverage_localFB
3774 ide_service
3775 tracking_id
3776 ref_unblocked_time
3777 editor_open_files
3778 params
3780 respond_jsonrpc
3781 ~powered_by:Serverless_ide
3783 (TypeCoverageResultFB result);
3784 Lwt.return_some
3786 result_count = List.length result.TypeCoverageFB.uncoveredRanges;
3787 result_extra_telemetry = None;
3789 (* Hover docblocks in serverless IDE *)
3790 | (_, Some ide_service, RequestMessage (id, HoverRequest params)) ->
3791 let%lwt () = cancel_if_stale client timestamp short_timeout in
3792 let%lwt result =
3793 do_hover_local
3794 ide_service
3795 tracking_id
3796 ref_unblocked_time
3797 editor_open_files
3798 params
3800 respond_jsonrpc ~powered_by:Serverless_ide id (HoverResult result);
3801 let result_count =
3802 match result with
3803 | None -> 0
3804 | Some { Hover.contents; _ } -> List.length contents
3806 Lwt.return_some { result_count; result_extra_telemetry = None }
3807 | (_, Some ide_service, RequestMessage (id, DocumentSymbolRequest params))
3809 let%lwt () = cancel_if_stale client timestamp short_timeout in
3810 let%lwt result =
3811 do_documentSymbol_local
3812 ide_service
3813 tracking_id
3814 ref_unblocked_time
3815 editor_open_files
3816 params
3818 respond_jsonrpc
3819 ~powered_by:Serverless_ide
3821 (DocumentSymbolResult result);
3822 Lwt.return_some
3823 { result_count = List.length result; result_extra_telemetry = None }
3824 | (_, Some ide_service, RequestMessage (id, DefinitionRequest params)) ->
3825 let%lwt () = cancel_if_stale client timestamp short_timeout in
3826 let%lwt result =
3827 do_definition_local
3828 ide_service
3829 tracking_id
3830 ref_unblocked_time
3831 editor_open_files
3832 params
3834 respond_jsonrpc ~powered_by:Serverless_ide id (DefinitionResult result);
3835 Lwt.return_some
3836 { result_count = List.length result; result_extra_telemetry = None }
3837 | (_, Some ide_service, RequestMessage (id, TypeDefinitionRequest params))
3839 let%lwt () = cancel_if_stale client timestamp short_timeout in
3840 let%lwt result =
3841 do_typeDefinition_local
3842 ide_service
3843 tracking_id
3844 ref_unblocked_time
3845 editor_open_files
3846 params
3848 respond_jsonrpc
3849 ~powered_by:Serverless_ide
3851 (TypeDefinitionResult result);
3852 Lwt.return_some
3853 { result_count = List.length result; result_extra_telemetry = None }
3854 (* Resolve documentation for a symbol: "Autocomplete Docblock!" *)
3855 | (_, Some ide_service, RequestMessage (id, SignatureHelpRequest params)) ->
3856 let%lwt () = cancel_if_stale client timestamp short_timeout in
3857 let%lwt result =
3858 do_signatureHelp_local
3859 ide_service
3860 tracking_id
3861 ref_unblocked_time
3862 editor_open_files
3863 params
3865 respond_jsonrpc ~powered_by:Serverless_ide id (SignatureHelpResult result);
3866 let result_count =
3867 match result with
3868 | None -> 0
3869 | Some { SignatureHelp.signatures; _ } -> List.length signatures
3871 Lwt.return_some { result_count; result_extra_telemetry = None }
3872 (* textDocument/formatting *)
3873 | (_, _, RequestMessage (id, DocumentFormattingRequest params)) ->
3874 let result = do_documentFormatting editor_open_files params in
3875 respond_jsonrpc
3876 ~powered_by:Language_server
3878 (DocumentFormattingResult result);
3879 Lwt.return_some
3880 { result_count = List.length result; result_extra_telemetry = None }
3881 (* textDocument/rangeFormatting *)
3882 | (_, _, RequestMessage (id, DocumentRangeFormattingRequest params)) ->
3883 let result = do_documentRangeFormatting editor_open_files params in
3884 respond_jsonrpc
3885 ~powered_by:Language_server
3887 (DocumentRangeFormattingResult result);
3888 Lwt.return_some
3889 { result_count = List.length result; result_extra_telemetry = None }
3890 (* textDocument/onTypeFormatting *)
3891 | (_, _, RequestMessage (id, DocumentOnTypeFormattingRequest params)) ->
3892 let%lwt () = cancel_if_stale client timestamp short_timeout in
3893 let result = do_documentOnTypeFormatting editor_open_files params in
3894 respond_jsonrpc
3895 ~powered_by:Language_server
3897 (DocumentOnTypeFormattingResult result);
3898 Lwt.return_some
3899 { result_count = List.length result; result_extra_telemetry = None }
3900 (* editor buffer events *)
3901 | ( _,
3903 NotificationMessage
3904 ( DidOpenNotification _ | DidChangeNotification _
3905 | DidCloseNotification _ | DidSaveNotification _ ) ) ->
3906 let%lwt () =
3907 handle_editor_buffer_message
3908 ~state:!state
3909 ~ide_service
3910 ~metadata
3911 ~ref_unblocked_time
3912 ~message
3914 Lwt.return_none
3915 (* any request/notification that we can't handle yet *)
3916 | (In_init _, _, message) ->
3917 (* we respond with Operation_cancelled so that clients don't produce *)
3918 (* user-visible logs/warnings. *)
3919 raise
3920 (Error.LspException
3922 Error.code = Error.RequestCancelled;
3923 message = Hh_server_initializing |> hh_server_state_to_string;
3924 data =
3925 Some
3926 (Hh_json.JSON_Object
3928 ("state", !state |> state_to_string |> Hh_json.string_);
3929 ( "message",
3930 Hh_json.string_
3931 (Lsp_fmt.denorm_message_to_string message) );
3934 (* textDocument/hover request *)
3935 | (Main_loop menv, _, RequestMessage (id, HoverRequest params)) ->
3936 let%lwt () = cancel_if_stale client timestamp short_timeout in
3937 let%lwt result = do_hover menv.conn ref_unblocked_time params in
3938 respond_jsonrpc ~powered_by:Hh_server id (HoverResult result);
3939 let result_count =
3940 match result with
3941 | None -> 0
3942 | Some { Hover.contents; _ } -> List.length contents
3944 Lwt.return_some { result_count; result_extra_telemetry = None }
3945 (* textDocument/typeDefinition request *)
3946 | (Main_loop menv, _, RequestMessage (id, TypeDefinitionRequest params)) ->
3947 let%lwt () = cancel_if_stale client timestamp short_timeout in
3948 let%lwt result = do_typeDefinition menv.conn ref_unblocked_time params in
3949 respond_jsonrpc ~powered_by:Hh_server id (TypeDefinitionResult result);
3950 Lwt.return_some
3951 { result_count = List.length result; result_extra_telemetry = None }
3952 (* textDocument/definition request *)
3953 | (Main_loop menv, _, RequestMessage (id, DefinitionRequest params)) ->
3954 let%lwt () = cancel_if_stale client timestamp short_timeout in
3955 let%lwt result =
3956 do_definition menv.conn ref_unblocked_time editor_open_files params
3958 respond_jsonrpc ~powered_by:Hh_server id (DefinitionResult result);
3959 Lwt.return_some
3960 { result_count = List.length result; result_extra_telemetry = None }
3961 (* textDocument/completion request *)
3962 | (Main_loop menv, _, RequestMessage (id, CompletionRequest params)) ->
3963 let do_completion =
3964 if env.use_ffp_autocomplete then
3965 do_completion_ffp
3966 else
3967 do_completion_legacy
3969 let%lwt () = cancel_if_stale client timestamp short_timeout in
3970 let%lwt result = do_completion menv.conn ref_unblocked_time params in
3971 respond_jsonrpc ~powered_by:Hh_server id (CompletionResult result);
3972 Lwt.return_some
3974 result_count = List.length result.Completion.items;
3975 result_extra_telemetry = None;
3977 (* completionItem/resolve request *)
3978 | ( Main_loop menv,
3980 RequestMessage (id, CompletionItemResolveRequest params) ) ->
3981 let%lwt () = cancel_if_stale client timestamp short_timeout in
3982 let%lwt result =
3983 do_completionItemResolve menv.conn ref_unblocked_time params
3985 respond_jsonrpc
3986 ~powered_by:Hh_server
3988 (CompletionItemResolveResult result);
3989 Lwt.return_some { result_count = 1; result_extra_telemetry = None }
3990 (* workspace/symbol request *)
3991 | (Main_loop menv, _, RequestMessage (id, WorkspaceSymbolRequest params)) ->
3992 let%lwt result = do_workspaceSymbol menv.conn ref_unblocked_time params in
3993 respond_jsonrpc ~powered_by:Hh_server id (WorkspaceSymbolResult result);
3994 Lwt.return_some
3995 { result_count = List.length result; result_extra_telemetry = None }
3996 (* textDocument/documentSymbol request *)
3997 | (Main_loop menv, _, RequestMessage (id, DocumentSymbolRequest params)) ->
3998 let%lwt result = do_documentSymbol menv.conn ref_unblocked_time params in
3999 respond_jsonrpc ~powered_by:Hh_server id (DocumentSymbolResult result);
4000 Lwt.return_some
4001 { result_count = List.length result; result_extra_telemetry = None }
4002 (* textDocument/references request *)
4003 | (Main_loop menv, _, RequestMessage (id, FindReferencesRequest params)) ->
4004 let%lwt () = cancel_if_stale client timestamp long_timeout in
4005 let%lwt result = do_findReferences menv.conn ref_unblocked_time params in
4006 respond_jsonrpc ~powered_by:Hh_server id (FindReferencesResult result);
4007 Lwt.return_some
4008 { result_count = List.length result; result_extra_telemetry = None }
4009 (* textDocument/implementation request *)
4010 | (Main_loop menv, _, RequestMessage (id, ImplementationRequest params)) ->
4011 let%lwt () = cancel_if_stale client timestamp long_timeout in
4012 let%lwt result =
4013 do_goToImplementation menv.conn ref_unblocked_time params
4015 respond_jsonrpc ~powered_by:Hh_server id (ImplementationResult result);
4016 Lwt.return_some
4017 { result_count = List.length result; result_extra_telemetry = None }
4018 (* textDocument/rename *)
4019 | (Main_loop menv, _, RequestMessage (id, RenameRequest params)) ->
4020 let%lwt result = do_documentRename menv.conn ref_unblocked_time params in
4021 respond_jsonrpc ~powered_by:Hh_server id (RenameResult result);
4022 let result_count =
4023 SMap.fold
4024 (fun _file changes tot -> tot + List.length changes)
4025 result.WorkspaceEdit.changes
4028 let result_extra_telemetry =
4029 Telemetry.create ()
4030 |> Telemetry.int_
4031 ~key:"files"
4032 ~value:(SMap.cardinal result.WorkspaceEdit.changes)
4034 Lwt.return_some
4035 { result_count; result_extra_telemetry = Some result_extra_telemetry }
4036 (* textDocument/documentHighlight *)
4037 | (Main_loop menv, _, RequestMessage (id, DocumentHighlightRequest params))
4039 let%lwt () = cancel_if_stale client timestamp short_timeout in
4040 let%lwt result =
4041 do_documentHighlight menv.conn ref_unblocked_time params
4043 respond_jsonrpc ~powered_by:Hh_server id (DocumentHighlightResult result);
4044 Lwt.return_some
4045 { result_count = List.length result; result_extra_telemetry = None }
4046 (* textDocument/typeCoverage *)
4047 | (Main_loop menv, _, RequestMessage (id, TypeCoverageRequestFB params)) ->
4048 let%lwt result = do_typeCoverageFB menv.conn ref_unblocked_time params in
4049 respond_jsonrpc ~powered_by:Hh_server id (TypeCoverageResultFB result);
4050 Lwt.return_some
4052 result_count = List.length result.TypeCoverageFB.uncoveredRanges;
4053 result_extra_telemetry = None;
4055 (* textDocument/toggleTypeCoverage *)
4056 | ( Main_loop menv,
4058 NotificationMessage (ToggleTypeCoverageNotificationFB params) ) ->
4059 let%lwt () =
4060 do_toggleTypeCoverageFB menv.conn ref_unblocked_time params
4062 Lwt.return_none
4063 (* textDocument/signatureHelp notification *)
4064 | (Main_loop menv, _, RequestMessage (id, SignatureHelpRequest params)) ->
4065 let%lwt result = do_signatureHelp menv.conn ref_unblocked_time params in
4066 respond_jsonrpc ~powered_by:Hh_server id (SignatureHelpResult result);
4067 let result_count =
4068 match result with
4069 | None -> 0
4070 | Some result -> List.length result.SignatureHelp.signatures
4072 Lwt.return_some { result_count; result_extra_telemetry = None }
4073 (* catch-all for client reqs/notifications we haven't yet implemented *)
4074 | (Main_loop _menv, _, message) ->
4075 let method_ = Lsp_fmt.message_name_to_string message in
4076 raise
4077 (Error.LspException
4079 Error.code = Error.MethodNotFound;
4080 message = Printf.sprintf "not implemented: %s" method_;
4081 data = None;
4083 (* catch-all for requests/notifications after shutdown request *)
4084 (* client message when we've lost the server *)
4085 | (Lost_server lenv, _, _) ->
4086 let open Lost_env in
4087 (* if trigger_on_lsp_method is set, our caller should already have *)
4088 (* transitioned away from this state. *)
4089 assert (not lenv.p.trigger_on_lsp);
4091 (* We deny all other requests. This is the only response that won't *)
4092 (* produce logs/warnings on most clients... *)
4093 raise
4094 (Error.LspException
4096 Error.code = Error.RequestCancelled;
4097 message = lenv.p.new_hh_server_state |> hh_server_state_to_string;
4098 data =
4099 Some
4100 (Hh_json.JSON_Object
4102 ("state", !state |> state_to_string |> Hh_json.string_);
4103 ( "message",
4104 Hh_json.string_
4105 (Lsp_fmt.denorm_message_to_string message) );
4109 Lwt.return result_telemetry_opt
4111 let handle_server_message
4112 ~(env : env) ~(state : state ref) ~(message : server_message) :
4113 result_telemetry option Lwt.t =
4114 let open Main_env in
4115 let%lwt () =
4116 match (!state, message) with
4117 (* server busy status *)
4118 | (_, { push = ServerCommandTypes.BUSY_STATUS status; _ }) ->
4119 (* if we're connected to hh_server, that can only be because
4120 we know its root, which can only be because we received initializeParams.
4121 So the following call won't fail! *)
4122 let p = initialize_params_exc () in
4123 let should_send_status =
4124 Lsp.Initialize.(p.initializationOptions.sendServerStatusEvents)
4126 ( if should_send_status then
4127 let status_message =
4128 let open ServerCommandTypes in
4129 match status with
4130 | Needs_local_typecheck -> "needs_local_typecheck"
4131 | Doing_local_typecheck -> "doing_local_typecheck"
4132 | Done_local_typecheck -> "done_local_typecheck"
4133 | Doing_global_typecheck _ -> "doing_global_typecheck"
4134 | Done_global_typecheck _ -> "done_global_typecheck"
4136 Lsp_helpers.telemetry_log to_stdout status_message );
4137 state := do_server_busy !state status;
4138 Lwt.return_unit
4139 (* textDocument/publishDiagnostics notification *)
4140 | (Main_loop menv, { push = ServerCommandTypes.DIAGNOSTIC (_, errors); _ })
4142 let uris_with_diagnostics =
4143 do_diagnostics menv.uris_with_diagnostics errors
4145 state := Main_loop { menv with uris_with_diagnostics };
4146 Lwt.return_unit
4147 (* any server diagnostics that come after we've shut down *)
4148 | (_, { push = ServerCommandTypes.DIAGNOSTIC _; _ }) -> Lwt.return_unit
4149 (* server shut-down request *)
4150 | (Main_loop _menv, { push = ServerCommandTypes.NEW_CLIENT_CONNECTED; _ })
4152 let%lwt new_state =
4153 do_lost_server
4154 ~env
4155 !state
4157 Lost_env.explanation = "hh_server is active in another window.";
4158 new_hh_server_state = Hh_server_stolen;
4159 start_on_click = false;
4160 trigger_on_lock_file = false;
4161 trigger_on_lsp = true;
4164 state := new_state;
4165 Lwt.return_unit
4166 (* server shut-down request, unexpected *)
4167 | (_, { push = ServerCommandTypes.NEW_CLIENT_CONNECTED; _ }) ->
4168 let message = "unexpected close of absent server" in
4169 let stack = "" in
4170 raise (Server_fatal_connection_exception { Marshal_tools.message; stack })
4171 (* server fatal shutdown *)
4172 | (_, { push = ServerCommandTypes.FATAL_EXCEPTION e; _ }) ->
4173 raise (Server_fatal_connection_exception e)
4174 (* server non-fatal exception *)
4175 | ( _,
4177 push =
4178 ServerCommandTypes.NONFATAL_EXCEPTION
4179 { Marshal_tools.message; stack };
4181 } ) ->
4182 let lsp_error =
4184 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
4185 message;
4186 data = Lsp_fmt.error_data_of_stack stack;
4189 raise (Server_nonfatal_exception lsp_error)
4191 Lwt.return_none
4193 let handle_server_hello ~(state : state ref) : result_telemetry option Lwt.t =
4194 let%lwt () =
4195 match !state with
4196 (* server completes initialization *)
4197 | In_init ienv ->
4198 let%lwt () = connect_after_hello ienv.In_init_env.conn !state in
4199 state := report_connect_end ienv;
4200 Lwt.return_unit
4201 (* any "hello" from the server when we weren't expecting it. This is so *)
4202 (* egregious that we can't trust anything more from the server. *)
4203 | _ ->
4204 let message = "Unexpected hello" in
4205 let stack = "" in
4206 raise (Server_fatal_connection_exception { Marshal_tools.message; stack })
4208 Lwt.return_none
4210 let handle_client_ide_notification
4211 ~(notification : ClientIdeMessage.notification) :
4212 result_telemetry option Lwt.t =
4213 let%lwt () =
4214 match notification with
4215 | ClientIdeMessage.Initializing
4216 | ClientIdeMessage.Processing_files _ ->
4217 (* Do nothing; these are handled by `ClientIdeService`. *)
4218 Lwt.return_unit
4219 | ClientIdeMessage.Done_processing ->
4220 Lsp_helpers.telemetry_log
4221 to_stdout
4222 "[client-ide] Done processing file changes";
4223 Lwt.return_unit
4225 Lwt.return_none
4227 let get_client_ide_status (ide_service : ClientIdeService.t) :
4228 ShowStatusFB.params option =
4229 let (type_, shortMessage, message, actions) =
4230 match ClientIdeService.get_status ide_service with
4231 | ClientIdeService.Status.Not_started ->
4232 ( MessageType.ErrorMessage,
4233 "Hack: not started",
4234 "Hack IDE: not started.",
4235 [{ ShowMessageRequest.title = client_ide_restart_button_text }] )
4236 | ClientIdeService.Status.Initializing ->
4237 ( MessageType.WarningMessage,
4238 "Hack: initializing",
4239 "Hack IDE: initializing.",
4240 [] )
4241 | ClientIdeService.Status.Processing_files p ->
4242 let open ClientIdeMessage.Processing_files in
4243 ( MessageType.WarningMessage,
4244 "Hack",
4245 Printf.sprintf "Hack IDE: processing %d files." p.total,
4246 [] )
4247 | ClientIdeService.Status.Ready ->
4248 (MessageType.InfoMessage, "Hack", "Hack IDE: ready.", [])
4249 | ClientIdeService.Status.Stopped s ->
4250 let open ClientIdeMessage in
4251 ( MessageType.ErrorMessage,
4252 "Hack: " ^ s.short_user_message,
4253 s.medium_user_message ^ see_output_hack,
4254 [{ ShowMessageRequest.title = client_ide_restart_button_text }] )
4256 Some
4258 ShowStatusFB.shortMessage = Some shortMessage;
4259 request = { ShowMessageRequest.type_; message; actions };
4260 progress = None;
4261 total = None;
4264 (** This function blocks while it attempts to connect to the monitor to read status.
4265 It normally it gets status quickly, but has a 3s timeout just in case. *)
4266 let get_hh_server_status (state : state ref) : ShowStatusFB.params option =
4267 let open ShowStatusFB in
4268 let open ShowMessageRequest in
4269 match !state with
4270 | Pre_init
4271 | Post_shutdown ->
4272 None
4273 | In_init ienv ->
4274 let open In_init_env in
4275 let time = Unix.time () in
4276 let delay_in_secs =
4277 if Sys_utils.is_test_mode () then
4278 (* we avoid raciness in our tests by not showing a real time *)
4279 "<test>"
4280 else
4281 int_of_float (time -. ienv.first_start_time) |> string_of_int
4283 (* TODO: better to report time that hh_server has spent initializing *)
4284 let (progress, warning) =
4285 match ServerUtils.server_progress ~timeout:3 (get_root_exn ()) with
4286 | Error _ -> (None, None)
4287 | Ok (progress, warning) -> (progress, warning)
4289 (* [progress] comes from ServerProgress.ml, sent to the monitor, and now we've fetched
4290 it from the monitor. It's a string "op X/Y units (%)" e.g. "typechecking 5/16 files (78%)",
4291 or None if there's no relevant progress to show.
4292 [warning] comes from the same place, and if pressent is a human-readable string
4293 that warns about saved-state-init failure. *)
4294 let progress =
4295 Option.value progress ~default:ClientConnect.default_progress_message
4297 let warning =
4298 if Option.is_some warning then
4299 " (saved-state not found - will take a while)"
4300 else
4303 let message =
4304 Printf.sprintf
4305 "hh_server initializing%s: %s [%s seconds]"
4306 warning
4307 progress
4308 delay_in_secs
4310 Some
4312 request = { type_ = MessageType.WarningMessage; message; actions = [] };
4313 progress = None;
4314 total = None;
4315 shortMessage = Some "Hack: initializing";
4317 | Main_loop { Main_env.hh_server_status; _ } ->
4318 (* This shows whether the connected hh_server is busy or ready.
4319 It's produced in clientLsp.do_server_busy upon receipt of a status
4320 enum from the server. See comments on hh_server_status for invariants. *)
4321 Some hh_server_status
4322 | Lost_server { Lost_env.p; _ } ->
4323 Some
4325 shortMessage = Some "Hack: stopped";
4326 request =
4328 type_ = MessageType.ErrorMessage;
4329 message = p.Lost_env.explanation;
4330 actions = [{ title = hh_server_restart_button_text }];
4332 progress = None;
4333 total = None;
4336 let hh_server_status_to_diagnostic
4337 (uri : documentUri option) (hh_server_status : ShowStatusFB.params) :
4338 PublishDiagnostics.params option =
4339 let open ShowStatusFB in
4340 let open ShowMessageRequest in
4341 let open PublishDiagnostics in
4342 let diagnostic =
4344 PublishDiagnostics.range =
4346 start = { line = 0; character = 0 };
4347 end_ = { line = 0; character = 1 };
4349 severity = None;
4350 code = NoCode;
4351 source = Some "hh_server";
4352 message = "";
4353 relatedInformation = [];
4354 relatedLocations = [];
4357 match (uri, hh_server_status.request.type_) with
4358 | (None, _)
4359 | (_, (MessageType.InfoMessage | MessageType.LogMessage)) ->
4360 None
4361 | (Some uri, MessageType.ErrorMessage) ->
4362 Some
4364 uri;
4365 isStatusFB = true;
4366 diagnostics =
4369 diagnostic with
4370 message =
4371 "hh_server isn't running, so there may be undetected errors. Try `hh` at the command line... "
4372 ^ hh_server_status.request.message;
4373 severity = Some Error;
4377 | (Some uri, MessageType.WarningMessage) ->
4378 Some
4380 uri;
4381 isStatusFB = true;
4382 diagnostics =
4385 diagnostic with
4386 message =
4387 "hh_server isn't yet ready, so there may undetected errors... "
4388 ^ hh_server_status.request.message;
4389 severity = Some Warning;
4394 (** Manages the state of which diagnostics have been shown to the user
4395 about hh_server status: removes the old one if necessary, and adds a new one
4396 if necessary. Note that we only display hh_server_status diagnostics
4397 during In_init and Lost_server states, neither of which have diagnostics
4398 of their own. *)
4399 let publish_hh_server_status_diagnostic
4400 (state : state) (hh_server_status : ShowStatusFB.params option) : state =
4401 let uri =
4402 match (get_most_recent_file state, get_editor_open_files state) with
4403 | (Some uri, Some open_files) when UriMap.mem uri open_files -> Some uri
4404 | (_, Some open_files) when not (UriMap.is_empty open_files) ->
4405 Some (UriMap.choose open_files |> fst)
4406 | (_, _) -> None
4408 let desired_diagnostic =
4409 Option.bind hh_server_status ~f:(hh_server_status_to_diagnostic uri)
4411 let get_existing_diagnostic state =
4412 match state with
4413 | In_init ienv -> ienv.In_init_env.hh_server_status_diagnostic
4414 | Lost_server lenv -> lenv.Lost_env.hh_server_status_diagnostic
4415 | _ -> None
4417 let publish_and_update_diagnostic state diagnostic =
4418 let notification = PublishDiagnosticsNotification diagnostic in
4419 notification |> print_lsp_notification |> to_stdout;
4420 match state with
4421 | In_init ienv ->
4422 In_init
4423 { ienv with In_init_env.hh_server_status_diagnostic = Some diagnostic }
4424 | Lost_server lenv ->
4425 Lost_server
4426 { lenv with Lost_env.hh_server_status_diagnostic = Some diagnostic }
4427 | _ -> state
4429 let open PublishDiagnostics in
4430 (* The following match emboodies these rules:
4431 (1) we only publish hh_server_status diagnostics in In_init and Lost_server states,
4432 (2) we'll remove the old PublishDiagnostic if necessary and add a new one if necessary
4433 (3) to avoid extra LSP messages, if the diagnostic hasn't changed then we won't send anything
4434 (4) to avoid flicker, if the diagnostic has changed but is still in the same file, then
4435 we refrain from sending an "erase old" message and it will be implied by sending "new". *)
4436 match (get_existing_diagnostic state, desired_diagnostic, state) with
4437 | (_, _, Main_loop _)
4438 | (_, _, Pre_init)
4439 | (_, _, Post_shutdown)
4440 | (None, None, _) ->
4441 state
4442 | (Some _, None, _) -> dismiss_diagnostics state
4443 | (Some existing, Some desired, _)
4444 when Lsp.equal_documentUri existing.uri desired.uri
4445 && Option.equal
4446 PublishDiagnostics.equal_diagnostic
4447 (List.hd existing.diagnostics)
4448 (List.hd desired.diagnostics) ->
4449 state
4450 | (Some existing, Some desired, _)
4451 when Lsp.equal_documentUri existing.uri desired.uri ->
4452 publish_and_update_diagnostic state desired
4453 | (Some _, Some desired, _) ->
4454 let state = dismiss_diagnostics state in
4455 publish_and_update_diagnostic state desired
4456 | (None, Some desired, _) -> publish_and_update_diagnostic state desired
4458 (** Here are the rules for merging status. They embody the principle that the spinner
4459 shows if initializing/typechecking is in progress, the error icon shows if error,
4460 and the status bar word is "Hack" if IDE services are available or "Hack: xyz" if not.
4461 Note that if Hack IDE is up but hh_server is down, then the hh_server failure message
4462 is conveyed via a publishDiagnostic; it's not conveyed via status.
4463 [ok] Hack -- if ide_service is up and hh_server is ready
4464 [spin] Hack -- if ide_service is processing-files or hh_server is initializing/typechecking
4465 [spin] Hack: initializing -- if ide_service is initializing
4466 [err] Hack: failure -- if ide_service is down
4467 If client_ide_service isn't enabled, then we show thing differently:
4468 [ok] Hack -- if hh_server is ready (Main_loop)
4469 [spin] Hack -- if hh_server is doing local or global typechecks (Main_loop)
4470 [spin] Hack: busy -- if hh_server is doing non-interruptible typechecks (Main_loop)
4471 [spin] Hack: initializing -- if hh_server is initializing (In_init)
4472 [err] hh_server: stopped -- hh_server is down (Lost_server)
4473 As for the tooltip and actions, they are combined from both ide_service and hh_server. *)
4474 let merge_statuses
4475 ~(client_ide_status : ShowStatusFB.params option)
4476 ~(hh_server_status : ShowStatusFB.params option) :
4477 ShowStatusFB.params option =
4478 (* The correctness of the following match is a bit subtle. This is how to think of it.
4479 From the spec in the docblock, (1) if there's no client_ide_service, then the result
4480 of this function is simply the same as hh_server_status, since that's how it was constructed
4481 by get_hh_server_status (for In_init and Lost_server) and do_server_busy; (2) if there
4482 is a client_ide_service then the result is almost always simply the same as ide_service
4483 since that's how it was constructed by get_client_ide_status; (3) the only exception to
4484 rule 2 is that, if client_ide_status would have shown "[ok] Hack" and hh_server_status
4485 would have been a spinner, then we change to "[spin] Hack". *)
4486 match (client_ide_status, hh_server_status) with
4487 | (None, None) -> None
4488 | (None, Some _) -> hh_server_status
4489 | (Some _, None) -> client_ide_status
4490 | (Some client_ide_status, Some hh_server_status) ->
4491 let open Lsp.ShowStatusFB in
4492 let open Lsp.ShowMessageRequest in
4493 let request =
4495 client_ide_status.request with
4496 message =
4497 client_ide_status.request.message
4498 ^ "\n"
4499 ^ hh_server_status.request.message;
4500 actions =
4501 client_ide_status.request.actions @ hh_server_status.request.actions;
4505 MessageType.equal client_ide_status.request.type_ MessageType.InfoMessage
4506 && MessageType.equal
4507 hh_server_status.request.type_
4508 MessageType.WarningMessage
4509 then
4510 let request = { request with type_ = MessageType.WarningMessage } in
4511 Some { client_ide_status with request; shortMessage = Some "Hack" }
4512 else
4513 Some { client_ide_status with request }
4515 let refresh_status
4516 ~(env : env)
4517 ~(state : state ref)
4518 ~(ide_service : ClientIdeService.t option ref)
4519 ~(init_id : string) : unit =
4520 if is_pre_init !state || is_post_shutdown !state then
4521 (* not allowed to send anything until we've received initialize event *)
4523 else
4524 let hh_server_status = get_hh_server_status state in
4525 let client_ide_status =
4526 match !ide_service with
4527 | None -> None
4528 | Some ide_service -> get_client_ide_status ide_service
4530 state := publish_hh_server_status_diagnostic !state hh_server_status;
4531 let status = merge_statuses ~hh_server_status ~client_ide_status in
4532 Option.iter
4533 status
4535 (request_showStatusFB
4536 ~on_result:(on_status_restart_action ~env ~init_id ~ide_service));
4539 let handle_tick
4540 ~(env : env) ~(state : state ref) ~(ref_unblocked_time : float ref) :
4541 result_telemetry option Lwt.t =
4542 let%lwt () =
4543 match !state with
4544 (* idle tick while waiting for server to complete initialization *)
4545 | In_init ienv ->
4546 let open In_init_env in
4547 let time = Unix.time () in
4548 let delay_in_secs = int_of_float (time -. ienv.most_recent_start_time) in
4549 let%lwt () =
4550 if delay_in_secs <= 10 then
4551 Lwt.return_unit
4552 else
4553 (* terminate + retry the connection *)
4554 let%lwt new_state = connect ~env !state in
4555 state := new_state;
4556 Lwt.return_unit
4558 Lwt.return_unit
4559 (* Tick when we're connected to the server *)
4560 | Main_loop menv ->
4561 let open Main_env in
4562 let%lwt () =
4563 if menv.needs_idle then begin
4564 (* If we're connected to a server and have no more messages in the queue, *)
4565 (* then we must let the server know we're idle, so it will be free to *)
4566 (* handle command-line requests. *)
4567 state := Main_loop { menv with needs_idle = false };
4568 let%lwt () =
4569 rpc menv.conn ref_unblocked_time ServerCommandTypes.IDE_IDLE
4571 Lwt.return_unit
4572 end else
4573 Lwt.return_unit
4575 Lwt.async EventLoggerLwt.flush;
4576 Lwt.return_unit
4577 (* idle tick. No-op. *)
4578 | _ ->
4579 Lwt.async EventLoggerLwt.flush;
4580 Lwt.return_unit
4582 Lwt.return_none
4584 let main (init_id : string) (env : env) : Exit_status.t Lwt.t =
4585 Printexc.record_backtrace true;
4586 from := env.from;
4587 HackEventLogger.set_from !from;
4589 if env.verbose then
4590 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Debug
4591 else
4592 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Error;
4593 set_verbose_to_file ~ide_service:None ~tracking_id:"[startup]" env.verbose;
4594 (* The --verbose flag in env.verbose is the only thing that controls verbosity
4595 to stderr. Meanwhile, verbosity-to-file can be altered dynamically by the user.
4596 Why are they different? because we should write to stderr under a test harness,
4597 but we should never write to stderr when invoked by VSCode - it's not even guaranteed
4598 to drain the stderr pipe. *)
4599 let ide_service =
4600 if env.use_serverless_ide then
4601 Some
4602 (ClientIdeService.make
4603 { ClientIdeMessage.init_id; verbose = env.verbose })
4604 else
4605 None
4607 let ide_service = ref ide_service in
4609 let client = Jsonrpc.make_queue () in
4610 let deferred_action : (unit -> unit Lwt.t) option ref = ref None in
4611 let state = ref Pre_init in
4612 let ref_event = ref None in
4613 let ref_unblocked_time = ref (Unix.gettimeofday ()) in
4614 (* ref_unblocked_time is the time at which we're no longer blocked on either *)
4615 (* clientLsp message-loop or hh_server, and can start actually handling. *)
4616 (* Everything that blocks will update this variable. *)
4617 let process_next_event () : unit Lwt.t =
4618 try%lwt
4619 let%lwt () =
4620 match !deferred_action with
4621 | Some deferred_action ->
4622 let%lwt () = deferred_action () in
4623 Lwt.return_unit
4624 | None -> Lwt.return_unit
4626 deferred_action := None;
4627 let%lwt event = get_next_event !state client !ide_service in
4628 ref_event := Some event;
4629 ref_unblocked_time := Unix.gettimeofday ();
4631 (* maybe set a flag to indicate that we'll need to send an idle message *)
4632 state := handle_idle_if_necessary !state event;
4634 (* if we're in a lost-server state, some triggers cause us to reconnect *)
4635 let%lwt new_state =
4636 reconnect_from_lost_if_necessary ~env !state (`Event event)
4638 state := new_state;
4640 (* we keep track of all open files and their contents *)
4641 state := track_open_and_recent_files !state event;
4643 (* we keep track of all files that have unsaved changes in them *)
4644 state := track_edits_if_necessary !state event;
4646 (* if a message comes from the server, maybe update our record of server state *)
4647 update_hh_server_state_if_necessary event;
4649 (* update status immediately if warranted *)
4650 refresh_status ~env ~state ~ide_service ~init_id;
4652 (* this is the main handler for each message*)
4653 let%lwt result_telemetry_opt =
4654 match event with
4655 | Client_message (metadata, message) ->
4656 handle_client_message
4657 ~env
4658 ~state
4659 ~client
4660 ~ide_service:!ide_service
4661 ~metadata
4662 ~message
4663 ~ref_unblocked_time
4664 | Client_ide_notification notification ->
4665 handle_client_ide_notification ~notification
4666 | Server_message message -> handle_server_message ~env ~state ~message
4667 | Server_hello -> handle_server_hello ~state
4668 | Tick -> handle_tick ~env ~state ~ref_unblocked_time
4670 (* for LSP requests and notifications, we keep a log of what+when we responded.
4671 INVARIANT: every LSP request gets either a response logged here,
4672 or an error logged by one of the handlers below. *)
4673 log_response_if_necessary
4675 event
4676 result_telemetry_opt
4677 !ref_unblocked_time;
4678 Lwt.return_unit
4679 with
4680 | Server_fatal_connection_exception { Marshal_tools.stack; message } ->
4681 if not (is_post_shutdown !state) then (
4682 (* The server never tells us why it closed the connection - it simply *)
4683 (* closes. We don't have privilege to inspect its exit status. *)
4684 (* But in some cases of a controlled exit, the server does write to a *)
4685 (* "finale file" to explain its reason for exit... *)
4686 let server_finale_data =
4687 match !state with
4688 | Main_loop { Main_env.conn; _ }
4689 | In_init { In_init_env.conn; _ } ->
4690 ClientConnect.get_finale_data conn.server_finale_file
4691 | _ -> None
4693 let server_finale_stack =
4694 match server_finale_data with
4695 | Some { ServerCommandTypes.stack = Utils.Callstack s; _ } -> s
4696 | _ -> ""
4698 let stack =
4699 Printf.sprintf
4700 "%s\n---\n%s\n---\n%s"
4701 stack
4702 (Printexc.get_backtrace ())
4703 server_finale_stack
4705 let e =
4707 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
4708 message;
4709 data = Lsp_fmt.error_data_of_stack stack;
4712 (* Log all the things! *)
4713 hack_log_error
4714 !ref_event
4716 Error_from_server_fatal
4717 !ref_unblocked_time
4718 env;
4719 Lsp_helpers.telemetry_error
4720 to_stdout
4721 (message ^ ", from_server\n" ^ stack);
4723 (* The monitor is responsible for detecting server closure and exit *)
4724 (* status, and restarting the server if necessary (that's not our job). *)
4725 (* All we'll do is put up a dialog telling the user that the server is *)
4726 (* down and giving them a button to restart. *)
4727 let explanation =
4728 match server_finale_data with
4729 | Some { ServerCommandTypes.msg; _ } -> msg
4730 | _ -> "hh_server: stopped."
4732 (* When would be a good time to auto-dismiss the dialog and attempt *)
4733 (* a proper re-connection? it's not our job to ascertain with certainty *)
4734 (* whether that re-connection will succeed - it's impossible to know, *)
4735 (* but also our re-connection attempt is pretty forceful. *)
4736 (* First: if the server determined in its finale that there shouldn't *)
4737 (* be automatic retry then we won't. Otherwise, we'll sleep for 1 sec *)
4738 (* and then look for the presence of the lock file. The sleep is *)
4739 (* because typically if you do "hh stop" then the persistent connection *)
4740 (* shuts down instantly but the monitor takes a short time to release *)
4741 (* its lockfile. *)
4742 let trigger_on_lock_file =
4743 match server_finale_data with
4744 | Some
4746 ServerCommandTypes.exit_status =
4747 Exit_status.Failed_to_load_should_abort;
4749 } ->
4750 false
4751 | _ -> true
4753 Unix.sleep 1;
4755 (* We're right now inside an exception handler. We don't want to do *)
4756 (* work that might itself throw. So instead we'll leave that to the *)
4757 (* next time around the loop. *)
4758 deferred_action :=
4759 Some
4760 (fun () ->
4761 let%lwt new_state =
4762 do_lost_server
4763 ~env
4764 !state
4766 Lost_env.explanation;
4767 new_hh_server_state = Hh_server_stopped;
4768 start_on_click = true;
4769 trigger_on_lock_file;
4770 trigger_on_lsp = false;
4773 state := new_state;
4774 Lwt.return_unit)
4776 Lwt.return_unit
4777 | Client_fatal_connection_exception { Marshal_tools.stack; message } ->
4778 let stack = stack ^ "---\n" ^ Printexc.get_backtrace () in
4779 let e =
4781 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
4782 message;
4783 data = Lsp_fmt.error_data_of_stack stack;
4786 hack_log_error
4787 !ref_event
4789 Error_from_client_fatal
4790 !ref_unblocked_time
4791 env;
4792 Lsp_helpers.telemetry_error to_stdout (message ^ ", from_client\n" ^ stack);
4793 let () = exit_fail () in
4794 Lwt.return_unit
4795 | Client_recoverable_connection_exception { Marshal_tools.stack; message }
4797 let stack = stack ^ "---\n" ^ Printexc.get_backtrace () in
4798 let e =
4800 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
4801 message;
4802 data = Lsp_fmt.error_data_of_stack stack;
4805 hack_log_error
4806 !ref_event
4808 Error_from_client_recoverable
4809 !ref_unblocked_time
4810 env;
4811 Lsp_helpers.telemetry_error to_stdout (message ^ ", from_client\n" ^ stack);
4812 Lwt.return_unit
4813 | (Server_nonfatal_exception e | Error.LspException e) as exn ->
4814 let exn = Exception.wrap exn in
4815 let error_source =
4816 match (e.Error.code, Exception.unwrap exn) with
4817 | (Error.RequestCancelled, _) -> Error_from_lsp_cancelled
4818 | (_, Server_nonfatal_exception _) -> Error_from_server_recoverable
4819 | (_, _) -> Error_from_lsp_misc
4821 let e = Lsp_fmt.add_stack_if_absent e exn in
4822 respond_to_error !ref_event e;
4823 hack_log_error !ref_event e error_source !ref_unblocked_time env;
4824 Lwt.return_unit
4825 | exn ->
4826 let exn = Exception.wrap exn in
4827 let e =
4829 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
4830 message = Exception.get_ctor_string exn;
4831 data = None;
4834 let e = Lsp_fmt.add_stack_if_absent e exn in
4835 respond_to_error !ref_event e;
4836 hack_log_error !ref_event e Error_from_lsp_misc !ref_unblocked_time env;
4837 Lwt.return_unit
4839 let rec main_loop () : unit Lwt.t =
4840 let%lwt () = process_next_event () in
4841 main_loop ()
4843 let%lwt () = main_loop () in
4844 Lwt.return Exit_status.No_error