2 * Copyright (c) 2015, Facebook, Inc.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
15 (* All hack-specific code relating to LSP goes in here. *)
19 config
: (string * string) list
;
20 use_ffp_autocomplete
: bool;
21 use_ranked_autocomplete
: bool;
22 use_serverless_ide
: 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
49 type hh_server_state
=
51 | Hh_server_initializing
52 | Hh_server_handling_or_ready
53 | Hh_server_denying_connection
55 | Hh_server_typechecking_local
56 | Hh_server_typechecking_global_blocking
57 | Hh_server_typechecking_global_interruptible
58 | Hh_server_typechecking_global_remote_blocking
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 *)
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;
88 ic
: Timeout.in_channel
;
90 server_finale_file
: string;
91 pending_messages
: server_message
Queue.t
;
92 (** ones that arrived during current rpc *)
95 module Main_env
= struct
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
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
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 *)
134 hh_server_status_diagnostic
: PublishDiagnostics.params
option;
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 *)
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... *)
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 =
168 | Post_shutdown
-> true
175 let is_pre_init (state
: state
) : bool =
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? *)
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
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 =
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"
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 =
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 =
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
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
278 | Client_ide_notification _
->
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. *)
295 Client_fatal_connection_exception
of Marshal_tools.remote_exception_data
298 Client_recoverable_connection_exception
of Marshal_tools.remote_exception_data
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 =
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
=
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 *)
354 let si_kind_to_completion_kind (kind
: SearchUtils.si_kind
) :
355 Completion.completionItemKind
option =
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
=
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 *)
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. *)
404 List.find
!hh_server_state ~f
:(fun (time
, _
) -> time
<= requested_time
)
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]"
413 let file = Filename.concat
(Path.to_string root
) ".hhconfig" in
414 let%lwt config
= Config_file_lwt.parse_hhconfig
file in
416 | Ok
(_hash
, config
) ->
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]"
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
=
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
443 let update_hh_server_state_if_necessary (event
: event
) : unit =
444 let open ServerCommandTypes
in
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
) ->
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
462 | NONFATAL_EXCEPTION _
->
466 | Server_message
{ push
; has_updated_server_state
= false } -> helper push
469 let rpc_lock = Lwt_mutex.create
()
472 (server_conn
: server_conn
)
473 (ref_unblocked_time
: float ref)
474 (command
: 'a
ServerCommandTypes.t
) : 'a
Lwt.t
=
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 });
481 server_conn
.pending_messages
482 { push
; has_updated_server_state
= true }
484 let start_time = Unix.gettimeofday
() in
486 ServerCommandLwt.rpc_persistent
487 (server_conn
.ic
, server_conn
.oc
)
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;
497 | Ok
((), res
, start_server_handle_time
) ->
498 ref_unblocked_time
:= start_server_handle_time
;
503 ServerCommandLwt.Remote_fatal_exception remote_e_data
) ->
504 raise
(Server_fatal_connection_exception remote_e_data
)
508 ServerCommandLwt.Remote_nonfatal_exception
509 { Marshal_tools.message
; stack
} ) ->
512 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
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
521 (Server_fatal_connection_exception
{ Marshal_tools.message; stack
}))
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 *)
531 (ide_service
: ClientIdeService.t
)
532 ~
(tracking_id
: string)
533 ~
(ref_unblocked_time
: float ref)
535 (message : 'a
ClientIdeMessage.t
) : 'a
Lwt.t
=
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
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
568 (ClientIdeMessage.Verbose
!verbose_to_file)
571 with _exn
-> Lwt.return_unit
572 (* TODO: log this *));
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
]
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
591 (* If no immediate messages are available, then wait up to 1 second. *)
593 Unix.descr_of_out_channel server
.oc
|> Lwt_unix.of_unix_file_descr
596 Jsonrpc.get_read_fd client
|> Lwt_unix.of_unix_file_descr
598 let%lwt message_source
=
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
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
=
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
641 let%lwt
() = Lwt_unix.sleep
1.1 in
642 failwith
"should have deferred to the `No_source case above"
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
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
660 | Response _
-> failwith
"unexpected response without request"
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"
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. *)
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
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
688 | `Message
{ Jsonrpc.json
; timestamp
} ->
691 let message = Lsp_fmt.parse_lsp json
get_outstanding_request_exn in
692 let rnd = Random_id.short_string
() in
695 | RequestMessage
(id
, _
) -> rnd ^
"." ^
Lsp_fmt.id_to_string id
698 Lwt.return
(Client_message
({ tracking_id; timestamp
}, message))
700 let e = Exception.wrap
e in
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)
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
719 let%lwt
message = from_client client
in
722 let%lwt
message = from_server conn
in
724 | `From_ide_service
message -> Lwt.return
message
725 | `No_source
-> Lwt.return Tick
)
727 let%lwt message_source
= get_client_message_source client ide_service
in
728 (match message_source
with
730 let%lwt
message = from_client client
in
732 | `From_ide_service
message -> Lwt.return
message
733 | `No_source
-> Lwt.return Tick
)
740 let add_powered_by ~
(powered_by
: powered_by
) (json
: Hh_json.json
) :
743 match (json
, powered_by
) with
744 | (JSON_Object props
, Serverless_ide
) ->
745 JSON_Object
(("powered_by", JSON_String
"serverless_ide") :: props
)
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
)
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
761 | Some
(Client_message
(_
, RequestMessage
(id
, _request
))) ->
762 respond_jsonrpc ~powered_by
:Language_server id
result
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
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
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
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 := "";
802 | ShowStatusResultFB
result -> on_result
result state
803 | ErrorResult error
-> on_error error state
807 Error.code
= Error.ParseError
;
808 message = "expected ShowStatusResult";
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
)
824 (titles
: string list
) : ShowMessageRequest.t
=
825 (* send the request *)
826 let id = NumberId
(Jsonrpc.get_next_request_id
()) in
828 List.map titles ~f
:(fun title
-> { ShowMessageRequest.title
})
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
=
837 | ShowMessageRequestResult
result -> on_result
result state
838 | ErrorResult
error -> on_error
error state
842 Error.code
= Error.ParseError
;
843 message = "expected ShowMessageRequestResult";
849 requests_outstanding := IdMap.add
id (request, handler) !requests_outstanding;
852 ShowMessageRequest.Present
{ id }
854 (** dismiss_showMessageRequest: sends a cancellation-request for the dialog *)
855 let dismiss_showMessageRequest (dialog
: ShowMessageRequest.t
) :
856 ShowMessageRequest.t
=
859 | ShowMessageRequest.Absent
-> ()
860 | ShowMessageRequest.Present
{ id; _
} ->
861 let notification = CancelRequestNotification
{ CancelRequest.id } in
862 let json = Lsp_fmt.print_lsp
(NotificationMessage
notification) in
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
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
}
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
->
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
) :
913 let open Lsp.TextDocumentPositionParams
in
914 let { Ide_api_types.line
; column
} = lsp_position_to_ide params.position
in
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
=
924 TextDocumentPositionParams.textDocument
= params.textDocument
;
925 position
= params.position
;
928 let hack_pos_to_lsp_range ~
(equal
: 'a
-> 'a
-> bool) (pos
: 'a
Pos.pos
) :
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 } }
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) :
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
=
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;
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
=
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) :
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.
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 =
1016 |> List.map ~f
:(fun (location, message) ->
1018 PublishDiagnostics.relatedLocation
= location;
1019 relatedMessage
= message;
1023 match Errors.get_severity
error with
1024 | Errors.Error
-> Some
PublishDiagnostics.Error
1025 | Errors.Warning
-> Some
PublishDiagnostics.Warning
1028 Lsp.PublishDiagnostics.range
;
1030 code
= PublishDiagnostics.IntCode
(Errors.get_code
error);
1031 source
= Some
"Hack";
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
:"";
1043 diagnostics
= List.map errors ~f
:hack_error_to_lsp_diagnostic;
1046 (************************************************************************)
1048 (************************************************************************)
1049 let get_document_contents
1050 (editor_open_files
: Lsp.TextDocumentItem.t
UriMap.t
) (uri
: documentUri
) :
1052 match UriMap.find_opt uri editor_open_files
with
1053 | Some document
-> Some document
.TextDocumentItem.text
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
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
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
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. *)
1091 log "Diag_subscribe: clientLsp do_shutdown unsubscribing diagnostic 0 ";
1096 (ServerCommandTypes.UNSUBSCRIBE_DIAGNOSTIC
0)
1098 let%lwt
() = rpc menv
.conn
(ref 0.0) ServerCommandTypes.DISCONNECT
in
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. *)
1106 (* No other states have a 'conn' to send any disconnect messages over. *)
1109 match ide_service
with
1110 | None
-> Lwt.return_unit
1111 | Some ide_service
->
1115 ~reason
:ClientIdeService.Stop_reason.Editor_exited
1117 Lwt.return Post_shutdown
1119 let state_to_rage (state : state) : string =
1123 | Post_shutdown
-> []
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";
1139 menv
.hh_server_status
.ShowStatusFB.shortMessage
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
->
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
;
1165 "new_hh_server_state";
1166 lenv
.p
.new_hh_server_state
|> hh_server_state_to_string;
1168 lenv
.p
.start_on_click
|> string_of_bool
;
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
=
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
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
1195 let stack = result.Lwt_utils.Process_success.stdout
in
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
1202 let stack = result.Lwt_utils.Process_success.stdout
in
1206 "unable to get pstack - " ^
e.Lwt_utils.Process_failure.stderr
1210 (* logfiles. Start them, but don't wait yet because we want this to run concurrently with fetching
1211 * the server logs. *)
1213 match get_root_opt () with
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");
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")
1231 (let%lwt
() = Lwt_unix.sleep
4.50 in
1232 Lwt.return
["Timed out while getting pstacks"]);
1234 |> List.filter ~f
:is_interesting
1235 |> Lwt_list.map_p
get_stack;
1238 List.iter stacks ~f
:add_data;
1241 let message = Exn.to_string
e in
1242 let stack = Printexc.get_backtrace
() in
1245 (Printf.sprintf
"Failed to get PIDs: %s - %s" message stack)))
1246 | None
-> Lwt.return_unit
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) =
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
1262 "[%02d:%02d:%02d.%03d] [%03.3fs ago] %s\n"
1270 let server_state_strings =
1271 List.map ~f
:server_state_to_string !hh_server_state
1276 ("LSP belief of hh_server_state:\n" :: server_state_strings));
1279 let server_promise =
1284 rpc menv
.conn
ref_unblocked_time ServerCommandTypes.RAGE
1288 { title
= i
.ServerRageTypes.title
; data
= i
.ServerRageTypes.data
}
1290 List.iter
items ~f
:add;
1292 | _
-> Lwt.return
(Error
"server rage - not in main loop")
1294 let timeout_promise =
1295 let%lwt
() = Lwt_unix.sleep
30. in
1297 Lwt.return
(Error
"server rage - timeout 30s")
1299 let%lwt server_rage_result
=
1300 try%lwt
Lwt.pick
[server_promise; timeout_promise]
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);
1314 (Str.regexp
"^\\^[0-9]+\\.[0-9]+\\.[0-9]+")
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;
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 *)
1332 ServerCommandTypes.DYNAMIC_VIEW
params.ToggleTypeCoverageFB.toggle
1334 cached_toggle_state := params.ToggleTypeCoverageFB.toggle
;
1335 rpc conn
ref_unblocked_time command
1338 (conn
: server_conn
)
1339 (ref_unblocked_time : float ref)
1340 (params : DidOpen.params) : unit Lwt.t
=
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
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
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;
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 =
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
)
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
1394 |> List.filter_map ~f
:(fun { HoverService.pos
; _
} -> pos
)
1396 |> Option.map ~f
:(hack_pos_to_lsp_range ~equal
:Relative_path.equal
)
1398 if List.is_empty
contents then
1401 Some
{ Hover.contents; range }
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
)
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
1420 ClientIdeService.rpc
1425 (ClientIdeMessage.Hover
document_location)
1429 let infos = do_hover_common infos in
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
1439 ServerCommandTypes.(IDENTIFY_TYPES
(LabelledFileName
file, line
, column
))
1441 let%lwt results
= rpc conn
ref_unblocked_time command in
1443 (List.map results ~f
:(fun nast_sid
->
1444 hack_pos_definition_to_lsp_identifier_location
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
1456 ClientIdeService.rpc
1461 (ClientIdeMessage.Type_definition
document_location)
1465 let file = Path.to_string
document_location.ClientIdeMessage.file_path in
1467 List.map
results ~f
:(fun nast_sid
->
1468 hack_pos_definition_to_lsp_identifier_location
1473 | Error
edata -> raise
(Server_nonfatal_exception
edata)
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
1482 params.TextDocumentPositionParams.textDocument
.TextDocumentIdentifier.uri
1485 match UriMap.find_opt
uri editor_open_files
with
1487 ServerCommandTypes.(
1489 { filename; content
= document
.TextDocumentItem.text })
1490 | None
-> ServerCommandTypes.(LabelledFileName
filename)
1493 ServerCommandTypes.GO_TO_DEFINITION
(labelled_file, line
, column
)
1495 let%lwt
results = rpc conn
ref_unblocked_time command in
1497 (List.map
results ~f
:(fun (_occurrence
, definition
) ->
1498 hack_symbol_definition_to_lsp_identifier_location
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
1510 ClientIdeService.rpc
1515 (ClientIdeMessage.Definition
document_location)
1520 List.map
results ~f
:(fun (_occurrence
, definition
) ->
1521 hack_symbol_definition_to_lsp_identifier_location
1524 (document_location.ClientIdeMessage.file_path |> Path.to_string
))
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
) :
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
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
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
=
1572 Initialize.(p.initializationOptions
.useTextEditAutocomplete
)
1574 match (completion
.func_details
, use_textedits) with
1576 when Lsp_helpers.supports_snippets
p
1577 && (not
is_caret_followed_by_lparen)
1579 (SearchUtils.equal_si_kind
1581 SearchUtils.SI_LocalVariable
) ->
1582 (* "method(${1:arg1}, ...)" but for args we just use param names. *)
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),
1590 | (_
, false) -> (`InsertText completion
.res_name
, PlainText
)
1596 range = ide_range_to_lsp completion
.res_replace_pos
;
1597 newText
= completion
.res_name
;
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
)
1610 if String.equal
(Pos.filename completion
.res_pos
) "" then
1611 Pos.set_file
filename completion
.res_pos
1616 let (line
, start
, _
) = Pos.info_pos
pos in
1617 let filename = Pos.filename pos in
1619 match completion
.res_base_class
with
1620 | Some
base_class -> [("base_class", Hh_json.JSON_String
base_class)]
1623 let ranking_detail =
1624 match completion
.ranking_details
with
1627 ("ranking_detail", Hh_json.JSON_String
details.detail
);
1628 ("ranking_source", Hh_json.JSON_Number
details.kind
);
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
1635 (Hh_json.JSON_Object
1636 ( [("fullname", Hh_json.JSON_String completion
.res_fullname
)]
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
);
1658 let hack_to_sort_text (completion
: complete_autocomplete_result
) :
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)
1674 ( completion
.res_name
1677 SearchUtils.equal_si_kind completion
.res_kind
SearchUtils.SI_Namespace
1683 (match completion
.ranking_details
with
1684 | Some _
-> Some
Completion.Event
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. *)
1693 (match completion
.ranking_details
with
1694 | Some detail
-> Some detail
.sort_text
1695 | None
-> hack_to_sort_text completion
);
1698 insertTextFormat
= Some insertTextFormat
;
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
1717 lsp_position_to_ide params.loc
.TextDocumentPositionParams.position
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
1733 lsp_position_to_ide params.loc
.TextDocumentPositionParams.position
1736 lsp_uri_to_path params.loc
.TextDocumentPositionParams.textDocument
.uri
1738 let is_manually_invoked =
1739 match params.context
with
1741 | Some c
-> is_invoked c
.triggerKind
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
1761 | Some c
-> is_invoked c
.triggerKind
1763 (* this is what I want to fix *)
1765 ClientIdeMessage.Completion
1766 { ClientIdeMessage.Completion.document_location; is_manually_invoked }
1769 ClientIdeService.rpc
1779 document_location.ClientIdeMessage.file_path |> Path.to_string
1781 let%lwt response
= make_ide_completion_response infos filename in
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
1793 (Core_kernel.List.fold docblock ~init
:[] ~
f:(fun acc elt
->
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
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
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
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)
1850 (* Okay let's get a docblock for this specific location *)
1852 ServerCommandTypes.DOCBLOCK_AT
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 *)
1863 let symbolname = params.Completion.label in
1864 let ranking_source =
1865 try Jget.int_opt
params.Completion.data "ranking_source"
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
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. *)
1902 match params.Completion.data with
1903 | None
-> raise NoLocationFound
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";
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
1935 (match location_result
with
1936 | Ok raw_docblock
->
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 *)
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
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"
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
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))
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;
2060 hack_symbol_definition_to_lsp_construct_location
2062 ~default_path
:filename;
2067 (* Flattens the recursive list of symbols *)
2068 | [] -> List.rev accu
2070 let children = Option.value def
.children ~default
:[] in
2071 let accu = hack_symbol_to_lsp def container_name
:: accu in
2073 hack_symbol_tree_to_lsp
2076 ~container_name
:(Some def
.name)
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
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;
2109 get_document_contents editor_open_files
params.textDocument
.uri;
2114 let request = ClientIdeMessage.Document_symbol
document_location in
2116 ClientIdeService.rpc
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 } =
2137 params.FindReferences.loc
.TextDocumentPositionParams.position
2140 Lsp_helpers.lsp_textDocumentIdentifier_to_filename
2141 params.FindReferences.loc
.TextDocumentPositionParams.textDocument
2144 params.FindReferences.context
.FindReferences.includeDeclaration
2146 let labelled_file = ServerCommandTypes.LabelledFileName
filename in
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 *)
2153 | None
-> Lwt.return
[]
2154 | Some
(_name
, positions
) ->
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
2166 Lsp_helpers.lsp_textDocumentIdentifier_to_filename
2167 params.TextDocumentPositionParams.textDocument
2169 let labelled_file = ServerCommandTypes.LabelledFileName
filename in
2171 ServerCommandTypes.IDE_GO_TO_IMPL
(labelled_file, line, column)
2173 let%lwt
results = rpc_with_retry conn
ref_unblocked_time command in
2175 | None
-> Lwt.return
[]
2176 | Some
(_name
, positions
) ->
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
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
2204 ClientIdeService.rpc
2209 (ClientIdeMessage.Document_highlight
document_location)
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
=
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
2221 (* We only show diagnostics for completely untypechecked code. *)
2222 | Ide_api_types.Checked
2223 | Ide_api_types.Partial
->
2225 | Ide_api_types.Unchecked
-> Some
{ range; message = None
}
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
=
2239 Lsp_helpers.lsp_textDocumentIdentifier_to_filename
params.textDocument
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
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
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 ->
2269 params.textDocument
.TextDocumentIdentifier.uri
2274 ClientIdeMessage.Type_coverage
2275 { ClientIdeMessage.file_path; ClientIdeMessage.file_contents }
2278 ClientIdeService.rpc
2286 | Ok
(results, counts
) ->
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
2306 ServerFormat.go_ide ~
filename_for_logging ~
content ~action ~options
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... *)
2322 { Error.code
= Error.UnknownErrorCode
; message; data = None
})
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
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.
2352 Character positions: 0 1 2 3 4 5 6
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.
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
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
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
2404 ClientIdeService.rpc
2409 (ClientIdeMessage.Signature_help
document_location)
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
2421 | Insert insert_patch
2422 | Replace insert_patch
->
2425 hack_pos_to_lsp_range ~equal
:String.equal insert_patch
.pos;
2426 newText = insert_patch
.text;
2430 TextEdit.range = hack_pos_to_lsp_range ~equal
:String.equal
pos;
2436 | Insert insert_patch
2437 | Replace insert_patch
->
2438 File_url.create
(filename insert_patch
.pos)
2439 | Remove
pos -> File_url.create
(filename pos)
2443 let patches_to_workspace_edit (patches
: ServerRefactorTypes.patch list
) :
2445 let changes = List.map patches ~
f:patch_to_workspace_edit_change in
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)
2462 let new_name = params.newName
in
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
2470 | Ok
patches -> patches
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
) :
2483 let open Main_env
in
2484 let open ServerCommandTypes
in
2485 let (type_
, shortMessage
, message) =
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
,
2494 "hh_server is initialized and running correctly." )
2495 | Doing_global_typecheck Blocking
->
2496 ( MessageType.WarningMessage
,
2498 "hh_server is typechecking the entire project (blocking)" )
2499 | Doing_global_typecheck Interruptible
->
2500 ( MessageType.WarningMessage
,
2502 "hh_server is typechecking entire project" )
2503 | Doing_global_typecheck
(Remote_blocking
message) ->
2504 ( MessageType.WarningMessage
,
2506 "hh_server is remote-typechecking the entire project - " ^
message )
2507 | Done_global_typecheck _
->
2508 ( MessageType.InfoMessage
,
2510 "hh_server is initialized and running correctly." )
2514 let hh_server_status =
2516 ShowStatusFB.shortMessage
= Some shortMessage
;
2517 request = { ShowMessageRequest.type_
; message; actions = [] };
2522 Main_loop
{ menv
with hh_server_status }
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. *)
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
2537 match SMap.find_opt
"" file_reports with
2538 | None
-> file_reports
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 *)
2559 List.map
files_without ~
f:(path_to_lsp_uri ~
default_path) |> UriSet.of_list
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";
2570 let _state = dismiss_diagnostics (In_init ienv
) in
2573 Main_env.conn
= ienv
.In_init_env.conn
;
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
;
2581 ShowStatusFB.request =
2583 ShowMessageRequest.type_
= MessageType.InfoMessage
;
2584 message = "hh_server: ready.";
2589 shortMessage
= None
;
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
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
2612 | ServerCommandTypes.Response
(ServerCommandTypes.Connected
, _
) ->
2613 set_hh_server_state Hh_server_handling_or_ready
2614 | _
-> failwith
"Didn't get server Connected response"
2617 (* tell server we want diagnostics *)
2618 log "Diag_subscribe: clientLsp subscribing diagnostic 0";
2620 rpc server_conn
ignore (ServerCommandTypes.SUBSCRIBE_DIAGNOSTIC
0)
2622 (* Extract the list of file changes we're tracking *)
2623 let editor_open_files =
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. *)
2636 (fun (uri, textDocument
) ->
2637 let filename = lsp_uri_to_path uri in
2639 ServerCommandTypes.OPEN_FILE
2640 (filename, textDocument
.TextDocumentItem.text)
2642 rpc server_conn
float_unblocked_time command)
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 })
2654 let rec connect_client ~
(env
: env
) (root
: Path.t
) ~
(autostart
: bool) :
2656 log "connect_client";
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. *)
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 *)
2675 (* only relevant when autostart=true *)
2676 log_inference_constraints
= false;
2678 profile_log
= false;
2683 (* only relevant when autostart=true *)
2684 progress_callback
= ClientConnect.null_progress_reporter
;
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
;
2693 config
= env
.config
;
2694 allow_non_opt_build
= false;
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
2718 server_capabilities
=
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
=
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. *) =
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
2787 try Sys.rename
client_lsp_log_fn (client_lsp_log_fn ^
".old")
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. *)
2808 watchman_debug_logging
= false;
2809 log_inference_constraints
= false;
2810 profile_log
= false;
2813 exit_on_failure
= false;
2815 ignore_hh_version
= false;
2816 saved_state_ignore_hhconfig
= false;
2817 dynamic_view
= !cached_toggle_state;
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
=
2834 | In_init
{ In_init_env.conn
; _
} ->
2837 Timeout.shutdown_connection conn
.ic
;
2838 Timeout.close_in_noerr conn
.ic
2844 | _
-> failwith
"connect only in Pre_init, In_init or Lost_server state"
2847 let%lwt conn
= connect_client ~env
(get_root_exn ()) ~autostart
:false in
2848 set_hh_server_state Hh_server_initializing
;
2853 { ienv
with In_init_env.conn
; most_recent_start_time
= Unix.time
() })
2855 let state = dismiss_diagnostics state in
2860 first_start_time
= Unix.time
();
2861 most_recent_start_time
= Unix.time
();
2862 most_recent_file
= get_most_recent_file state;
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
;
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
2888 "connect failed: %s [%s]\n%s"
2890 (Lsp.Error.show_code code
)
2893 let () = Lsp_helpers.telemetry_error
to_stdout longMessage in
2895 let new_hh_server_state =
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
->
2902 | Exit_with Out_of_retries
2903 | Exit_with Out_of_time
->
2904 Hh_server_denying_connection
2905 | _
-> Hh_server_unknown
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
2918 ~allow_immediate_reconnect
:false
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;
2930 and reconnect_from_lost_if_necessary
2931 ~
(env
: env
) (state : state) (reason
: [> `Event
of event
| `Force_regain
])
2934 let should_reconnect =
2935 match (state, reason
) with
2936 | (Lost_server _
, `Force_regain
) -> true
2937 | ( Lost_server
{ p = { trigger_on_lsp
= true; _
}; _
},
2939 (Client_message
(_
, (RequestMessage _
| NotificationMessage _
))) )
2942 | ( Lost_server
{ p = { trigger_on_lock_file
= true; _
}; lock_file
; _
},
2944 MonitorConnection.server_exists lock_file
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
2958 if List.is_empty
unsaved then
2961 unsaved |> List.map ~
f:string_of_uri
|> String.concat ~sep
:"\n"
2966 ^
"\nVersion in hhconfig that spawned the current hh_client: "
2968 ^
"\nVersion in hhconfig currently: "
2972 Lsp_helpers.telemetry_log
to_stdout message;
2975 let%lwt
state = connect ~env
state in
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. *)
2986 ?
(allow_immediate_reconnect
= true)
2987 (p : Lost_env.params) : state Lwt.t
=
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 (
3010 uris_with_unsaved_changes;
3012 hh_server_status_diagnostic
= None
;
3015 Lsp_helpers.telemetry_log
3017 "Reconnecting immediately to hh_server";
3019 reconnect_from_lost_if_necessary ~env
lost_state `Force_regain
3021 Lwt.return new_state
3029 uris_with_unsaved_changes;
3031 hh_server_status_diagnostic
= None
;
3034 let handle_idle_if_necessary (state : state) (event
: event
) : state =
3036 | Main_loop
menv when not
(is_tick event
) ->
3037 Main_loop
{ menv with Main_env.needs_idle
= true }
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 =
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)) ->
3053 params.DidChange.textDocument
.VersionedTextDocumentIdentifier.uri
3055 let doc = UriMap.find_opt
uri prev_opened_files in
3056 let open Lsp.TextDocumentItem
in
3063 params.DidChange.textDocument
3064 .VersionedTextDocumentIdentifier.version;
3066 Lsp_helpers.apply_changes_unsafe
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 =
3081 | Client_message
(_metadata
, message) ->
3082 let uri = Lsp_fmt.get_uri_opt
message in
3083 if Option.is_some
uri then
3086 get_most_recent_file state
3087 | _
-> get_most_recent_file state
3091 Main_loop
{ menv with Main_env.editor_open_files; most_recent_file }
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 }
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 =
3104 | Client_message
(_
, NotificationMessage
(DidChangeNotification
params)) ->
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
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 }
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
3131 let path = Lsp_helpers.lsp_uri_to_path uri in
3132 Some
(Relative_path.create_detect_prefix
path)
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
) :
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
3148 (result_telemetry_opt
: result_telemetry
option)
3149 (unblocked_time
: float) : unit =
3151 | Client_message
(metadata
, message) ->
3152 let (kind, method_
) = get_message_kind_and_method_for_logging message in
3153 let t = Unix.gettimeofday
() in
3155 "lsp-message [%s] queue time [%0.3f] execution time [%0.3f"
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 ())
3169 ~path_opt
:(get_filename_in_message_for_logging message)
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
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
3190 (event
: event
option)
3192 (source
: error_source
)
3193 (unblocked_time
: float)
3194 (env
: env
) : unit =
3195 let root = get_root_opt () in
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
->
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);
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
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
3236 HackEventLogger.client_lsp_exception
3238 ~
message:e.Error.message
3239 ~data_opt
:e.Error.data
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
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
3258 Error.code
= Error.RequestCancelled
;
3259 message = "request timed out";
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. *)
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
3285 initialize_params.client_capabilities
.workspace
.didChangeWatchedFiles
3286 .dynamicRegistration
)
3288 log "Language client reports that it supports file-watching"
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 =
3298 initialize_params.initializationOptions
.namingTableSavedStatePath
)
3299 |> Option.map ~
f:Path.make
3303 |> Option.value ~default
:UriMap.empty
3305 |> List.map ~
f:(fun uri -> uri |> lsp_uri_to_path |> Path.make
)
3308 ClientIdeService.initialize_from_saved_state
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
3318 | Ok num_changed_files_to_process
->
3319 Lsp_helpers.telemetry_log
3322 "[client-ide] Initialized; %d file changes to process"
3323 num_changed_files_to_process
);
3324 let%lwt
() = ClientIdeService.serve ide_service
in
3328 ClientIdeMessage.medium_user_message
;
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
3337 match upload_result
with
3338 | Ok url
-> Printf.sprintf
"\nMore details: %s" url
3341 "\n\nMore details:\n%s\n\nTried to upload those details but it didn't work...\n%s"
3346 "IDE services could not be initialized.\n%s\n%s"
3349 Lsp_helpers.log_error
to_stdout (long_user_message ^
append_to_log);
3350 if is_actionable
then
3351 Lsp_helpers.showMessage_error
3353 (medium_user_message ^
see_output_hack);
3357 let on_status_restart_action
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
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;
3388 ~ide_service
:(Some
new_ide_service)
3389 ~
tracking_id:"[restart]"
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 () ->
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. *)
3405 ~
tracking_id:"restart"
3406 ~reason
:ClientIdeService.Stop_reason.Restarting
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
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
3434 (* textDocument/didClose notification *)
3435 | (Main_loop
menv, NotificationMessage
(DidCloseNotification
params)) ->
3436 let%lwt
() = do_didClose menv.conn
ref_hh_unblocked_time params in
3438 (* textDocument/didChange notification *)
3439 | (Main_loop
menv, NotificationMessage
(DidChangeNotification
params)) ->
3440 let%lwt
() = do_didChange menv.conn
ref_hh_unblocked_time params in
3442 (* textDocument/didSave notification *)
3443 | (Main_loop _menv
, NotificationMessage
(DidSaveNotification _params
)) ->
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)) ->
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. *)
3463 ~
tracking_id:metadata
.tracking_id
3464 ~
ref_unblocked_time:ref_ide_unblocked_time
3466 ClientIdeMessage.(Ide_file_opened
{ file_path; file_contents })
3469 | (Some ide_service
, NotificationMessage
(DidChangeNotification
params)) ->
3472 params.DidChange.textDocument
.VersionedTextDocumentIdentifier.uri
3477 ~
tracking_id:metadata
.tracking_id
3478 ~
ref_unblocked_time:ref_ide_unblocked_time
3480 ClientIdeMessage.(Ide_file_changed
{ Ide_file_changed.file_path })
3483 | (Some ide_service
, NotificationMessage
(DidCloseNotification
params)) ->
3485 uri_to_path params.DidClose.textDocument
.TextDocumentIdentifier.uri
3490 ~
tracking_id:metadata
.tracking_id
3491 ~
ref_unblocked_time:ref_ide_unblocked_time
3493 ClientIdeMessage.(Ide_file_closed
file_path)
3497 (* Don't handle other events for now. When we show typechecking errors for
3498 the open file, we'll start handling them. *)
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) =
3509 let%lwt
() = hh_server_promise
in
3511 with e -> Lwt.return_some
(Exception.wrap
e)
3512 and (ide_service_e
: Exception.t option) =
3514 let%lwt
() = ide_service_promise
in
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
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
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
3548 | (_
, _
, ResponseMessage
(id, response)) ->
3549 let (_
, handler) = IdMap.find
id !requests_outstanding in
3550 let%lwt new_state
= handler response !state in
3553 (* shutdown request *)
3554 | (_
, _
, RequestMessage
(id, ShutdownRequest
)) ->
3556 do_shutdown !state ide_service
tracking_id ref_unblocked_time
3559 respond_jsonrpc ~powered_by
:Language_server
id ShutdownResult
;
3561 (* cancel notification *)
3562 | (_
, _
, NotificationMessage
(CancelRequestNotification _
)) ->
3563 (* For now, we'll ignore it. *)
3565 (* exit notification *)
3566 | (_
, _
, NotificationMessage ExitNotification
) ->
3567 if is_post_shutdown !state then
3571 (* setTrace notification *)
3572 | (_
, _
, NotificationMessage
(SetTraceNotification
params)) ->
3575 | SetTraceNotification.Verbose
-> true
3576 | SetTraceNotification.Off
-> false
3578 set_verbose_to_file ~ide_service ~
tracking_id value;
3580 (* test entrypoint: shutdown client_ide_service *)
3583 RequestMessage
(id, HackTestShutdownServerlessRequestFB
) ) ->
3588 ~reason
:ClientIdeService.Stop_reason.Testing
3591 ~powered_by
:Serverless_ide
3593 HackTestShutdownServerlessResultFB
;
3595 (* test entrypoint: stop hh_server *)
3596 | (_
, _
, RequestMessage
(id, HackTestStopServerRequestFB
)) ->
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
;
3603 (* test entrypoint: start hh_server *)
3604 | (_
, _
, RequestMessage
(id, HackTestStartServerRequestFB
)) ->
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
;
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. *)
3623 initialize_params.client_capabilities
.textDocument
.declaration
3624 .declarationLinkSupport
)
3625 && String.equal env
.from "vscode"
3627 from := "vscode_pre314";
3628 HackEventLogger.set_from
!from
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
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. *)
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
3647 let result = do_initialize ~env
root in
3648 respond_jsonrpc ~powered_by
:Language_server
id (InitializeResult
result);
3651 match ide_service
with
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
3667 if not
@@ Sys_utils.is_test_mode
() then
3668 Lsp_helpers.telemetry_log
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
, _
, _
) ->
3677 Error.code
= Error.ServerNotInitialized
;
3678 message = "Server not yet initialized";
3681 | (Post_shutdown
, _
, _c
) ->
3685 Error.code
= Error.InvalidRequest
;
3686 message = "already received shutdown request";
3689 (* initialized notification *)
3690 | (_
, _
, NotificationMessage InitializedNotification
) -> Lwt.return_none
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);
3696 { result_count
= List.length
result; result_extra_telemetry
= None
}
3699 NotificationMessage
(DidChangeWatchedFilesNotification
notification) )
3701 let open DidChangeWatchedFiles
in
3703 List.map
notification.changes ~
f:(fun change
->
3704 change
.uri |> lsp_uri_to_path |> Path.make
)
3712 ClientIdeMessage.(Disk_files_changed
changes)
3715 (* Text document completion: "AutoComplete!" *)
3716 | (_
, Some ide_service
, RequestMessage
(id, CompletionRequest
params)) ->
3717 let%lwt
() = cancel_if_stale client
timestamp short_timeout in
3726 respond_jsonrpc ~powered_by
:Serverless_ide
id (CompletionResult
result);
3729 result_count
= List.length
result.Completion.items;
3730 result_extra_telemetry
= None
;
3732 (* Resolve documentation for a symbol: "Autocomplete Docblock!" *)
3735 RequestMessage
(id, CompletionItemResolveRequest
params) ) ->
3736 let%lwt
() = cancel_if_stale client
timestamp short_timeout in
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
3763 ~powered_by
:Serverless_ide
3765 (DocumentHighlightResult
result);
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
3773 do_typeCoverage_localFB
3781 ~powered_by
:Serverless_ide
3783 (TypeCoverageResultFB
result);
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
3800 respond_jsonrpc ~powered_by
:Serverless_ide
id (HoverResult
result);
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
3811 do_documentSymbol_local
3819 ~powered_by
:Serverless_ide
3821 (DocumentSymbolResult
result);
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
3834 respond_jsonrpc ~powered_by
:Serverless_ide
id (DefinitionResult
result);
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
3841 do_typeDefinition_local
3849 ~powered_by
:Serverless_ide
3851 (TypeDefinitionResult
result);
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
3858 do_signatureHelp_local
3865 respond_jsonrpc ~powered_by
:Serverless_ide
id (SignatureHelpResult
result);
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
3876 ~powered_by
:Language_server
3878 (DocumentFormattingResult
result);
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
3885 ~powered_by
:Language_server
3887 (DocumentRangeFormattingResult
result);
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
3895 ~powered_by
:Language_server
3897 (DocumentOnTypeFormattingResult
result);
3899 { result_count = List.length
result; result_extra_telemetry
= None
}
3900 (* editor buffer events *)
3904 ( DidOpenNotification _
| DidChangeNotification _
3905 | DidCloseNotification _
| DidSaveNotification _
) ) ->
3907 handle_editor_buffer_message
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. *)
3922 Error.code
= Error.RequestCancelled
;
3923 message = Hh_server_initializing
|> hh_server_state_to_string;
3926 (Hh_json.JSON_Object
3928 ("state", !state |> state_to_string |> 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);
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);
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
3956 do_definition menv.conn
ref_unblocked_time editor_open_files params
3958 respond_jsonrpc ~powered_by
:Hh_server
id (DefinitionResult
result);
3960 { result_count = List.length
result; result_extra_telemetry
= None
}
3961 (* textDocument/completion request *)
3962 | (Main_loop
menv, _
, RequestMessage
(id, CompletionRequest
params)) ->
3964 if env
.use_ffp_autocomplete
then
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);
3974 result_count = List.length
result.Completion.items;
3975 result_extra_telemetry
= None
;
3977 (* completionItem/resolve request *)
3980 RequestMessage
(id, CompletionItemResolveRequest
params) ) ->
3981 let%lwt
() = cancel_if_stale client
timestamp short_timeout in
3983 do_completionItemResolve menv.conn
ref_unblocked_time params
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);
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);
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);
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
4013 do_goToImplementation menv.conn
ref_unblocked_time params
4015 respond_jsonrpc ~powered_by
:Hh_server
id (ImplementationResult
result);
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);
4024 (fun _file
changes tot
-> tot
+ List.length
changes)
4025 result.WorkspaceEdit.changes
4028 let result_extra_telemetry =
4032 ~
value:(SMap.cardinal
result.WorkspaceEdit.changes)
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
4041 do_documentHighlight menv.conn
ref_unblocked_time params
4043 respond_jsonrpc ~powered_by
:Hh_server
id (DocumentHighlightResult
result);
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);
4052 result_count = List.length
result.TypeCoverageFB.uncoveredRanges
;
4053 result_extra_telemetry = None
;
4055 (* textDocument/toggleTypeCoverage *)
4058 NotificationMessage
(ToggleTypeCoverageNotificationFB
params) ) ->
4060 do_toggleTypeCoverageFB menv.conn
ref_unblocked_time params
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);
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
4079 Error.code
= Error.MethodNotFound
;
4080 message = Printf.sprintf
"not implemented: %s" method_;
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... *)
4096 Error.code
= Error.RequestCancelled
;
4097 message = lenv
.p.new_hh_server_state |> hh_server_state_to_string;
4100 (Hh_json.JSON_Object
4102 ("state", !state |> state_to_string |> 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
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
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
;
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 };
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
; _
})
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;
4166 (* server shut-down request, unexpected *)
4167 | (_
, { push
= ServerCommandTypes.NEW_CLIENT_CONNECTED
; _
}) ->
4168 let message = "unexpected close of absent server" 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 *)
4178 ServerCommandTypes.NONFATAL_EXCEPTION
4179 { Marshal_tools.message; stack };
4184 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
4186 data = Lsp_fmt.error_data_of_stack
stack;
4189 raise
(Server_nonfatal_exception
lsp_error)
4193 let handle_server_hello ~
(state : state ref) : result_telemetry
option Lwt.t =
4196 (* server completes initialization *)
4198 let%lwt
() = connect_after_hello ienv
.In_init_env.conn
!state in
4199 state := report_connect_end ienv
;
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. *)
4204 let message = "Unexpected hello" in
4206 raise
(Server_fatal_connection_exception
{ Marshal_tools.message; stack })
4210 let handle_client_ide_notification
4211 ~
(notification : ClientIdeMessage.notification) :
4212 result_telemetry
option Lwt.t =
4214 match notification with
4215 | ClientIdeMessage.Initializing
4216 | ClientIdeMessage.Processing_files _
->
4217 (* Do nothing; these are handled by `ClientIdeService`. *)
4219 | ClientIdeMessage.Done_processing
->
4220 Lsp_helpers.telemetry_log
4222 "[client-ide] Done processing file changes";
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.",
4241 | ClientIdeService.Status.Processing_files
p ->
4242 let open ClientIdeMessage.Processing_files
in
4243 ( MessageType.WarningMessage
,
4245 Printf.sprintf
"Hack IDE: processing %d files." p.total
,
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 }] )
4258 ShowStatusFB.shortMessage
= Some shortMessage
;
4259 request = { ShowMessageRequest.type_
; message; actions };
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
4274 let open In_init_env
in
4275 let time = Unix.time () in
4277 if Sys_utils.is_test_mode
() then
4278 (* we avoid raciness in our tests by not showing a real time *)
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. *)
4295 Option.value progress ~default
:ClientConnect.default_progress_message
4298 if Option.is_some
warning then
4299 " (saved-state not found - will take a while)"
4305 "hh_server initializing%s: %s [%s seconds]"
4312 request = { type_
= MessageType.WarningMessage
; message; actions = [] };
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; _
} ->
4325 shortMessage
= Some
"Hack: stopped";
4328 type_
= MessageType.ErrorMessage
;
4329 message = p.Lost_env.explanation;
4330 actions = [{ title
= hh_server_restart_button_text }];
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
4344 PublishDiagnostics.range =
4346 start
= { line = 0; character
= 0 };
4347 end_
= { line = 0; character
= 1 };
4351 source = Some
"hh_server";
4353 relatedInformation = [];
4354 relatedLocations
= [];
4357 match (uri, hh_server_status.request.type_
) with
4359 | (_
, (MessageType.InfoMessage
| MessageType.LogMessage
)) ->
4361 | (Some
uri, MessageType.ErrorMessage
) ->
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
) ->
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
4399 let publish_hh_server_status_diagnostic
4400 (state : state) (hh_server_status : ShowStatusFB.params option) : state =
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
)
4408 let desired_diagnostic =
4409 Option.bind
hh_server_status ~
f:(hh_server_status_to_diagnostic uri)
4411 let get_existing_diagnostic state =
4413 | In_init ienv
-> ienv
.In_init_env.hh_server_status_diagnostic
4414 | Lost_server lenv
-> lenv
.Lost_env.hh_server_status_diagnostic
4417 let publish_and_update_diagnostic state diagnostic =
4418 let notification = PublishDiagnosticsNotification
diagnostic in
4419 notification |> print_lsp_notification
|> to_stdout;
4423 { ienv
with In_init_env.hh_server_status_diagnostic
= Some
diagnostic }
4424 | Lost_server lenv
->
4426 { lenv
with Lost_env.hh_server_status_diagnostic
= Some
diagnostic }
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 _
)
4439 | (_
, _
, Post_shutdown
)
4440 | (None
, None
, _
) ->
4442 | (Some _
, None
, _
) -> dismiss_diagnostics state
4443 | (Some existing
, Some desired
, _
)
4444 when Lsp.equal_documentUri existing
.uri desired
.uri
4446 PublishDiagnostics.equal_diagnostic
4447 (List.hd existing
.diagnostics
)
4448 (List.hd desired
.diagnostics
) ->
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. *)
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
4495 client_ide_status
.request with
4497 client_ide_status
.request.message
4499 ^
hh_server_status.request.message;
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
4510 let request = { request with type_
= MessageType.WarningMessage
} in
4511 Some
{ client_ide_status
with request; shortMessage
= Some
"Hack" }
4513 Some
{ client_ide_status
with request }
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 *)
4524 let hh_server_status = get_hh_server_status state in
4525 let client_ide_status =
4526 match !ide_service
with
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
4535 (request_showStatusFB
4536 ~on_result
:(on_status_restart_action ~env ~init_id ~ide_service
));
4540 ~
(env
: env
) ~
(state : state ref) ~
(ref_unblocked_time : float ref) :
4541 result_telemetry
option Lwt.t =
4544 (* idle tick while waiting for server to complete initialization *)
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
4550 if delay_in_secs <= 10 then
4553 (* terminate + retry the connection *)
4554 let%lwt new_state
= connect ~env
!state in
4559 (* Tick when we're connected to the server *)
4561 let open Main_env
in
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 };
4569 rpc menv.conn
ref_unblocked_time ServerCommandTypes.IDE_IDLE
4575 Lwt.async
EventLoggerLwt.flush
;
4577 (* idle tick. No-op. *)
4579 Lwt.async
EventLoggerLwt.flush
;
4584 let main (init_id
: string) (env
: env
) : Exit_status.t Lwt.t =
4585 Printexc.record_backtrace
true;
4587 HackEventLogger.set_from
!from;
4590 Hh_logger.Level.set_min_level_stderr
Hh_logger.Level.Debug
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. *)
4600 if env
.use_serverless_ide
then
4602 (ClientIdeService.make
4603 { ClientIdeMessage.init_id
; verbose
= env
.verbose
})
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 =
4620 match !deferred_action with
4621 | Some
deferred_action ->
4622 let%lwt
() = deferred_action () in
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 *)
4636 reconnect_from_lost_if_necessary ~env
!state (`Event event
)
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
=
4655 | Client_message
(metadata
, message) ->
4656 handle_client_message
4660 ~
ide_service:!ide_service
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
4676 result_telemetry_opt
4677 !ref_unblocked_time;
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 =
4688 | Main_loop
{ Main_env.conn
; _
}
4689 | In_init
{ In_init_env.conn
; _
} ->
4690 ClientConnect.get_finale_data conn
.server_finale_file
4693 let server_finale_stack =
4694 match server_finale_data with
4695 | Some
{ ServerCommandTypes.stack = Utils.Callstack
s; _
} -> s
4700 "%s\n---\n%s\n---\n%s"
4702 (Printexc.get_backtrace
())
4707 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
4709 data = Lsp_fmt.error_data_of_stack
stack;
4712 (* Log all the things! *)
4716 Error_from_server_fatal
4719 Lsp_helpers.telemetry_error
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. *)
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 *)
4742 let trigger_on_lock_file =
4743 match server_finale_data with
4746 ServerCommandTypes.exit_status
=
4747 Exit_status.Failed_to_load_should_abort
;
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. *)
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;
4777 | Client_fatal_connection_exception
{ Marshal_tools.stack; message } ->
4778 let stack = stack ^
"---\n" ^
Printexc.get_backtrace
() in
4781 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
4783 data = Lsp_fmt.error_data_of_stack
stack;
4789 Error_from_client_fatal
4792 Lsp_helpers.telemetry_error
to_stdout (message ^
", from_client\n" ^
stack);
4793 let () = exit_fail () in
4795 | Client_recoverable_connection_exception
{ Marshal_tools.stack; message }
4797 let stack = stack ^
"---\n" ^
Printexc.get_backtrace
() in
4800 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
4802 data = Lsp_fmt.error_data_of_stack
stack;
4808 Error_from_client_recoverable
4811 Lsp_helpers.telemetry_error
to_stdout (message ^
", from_client\n" ^
stack);
4813 | (Server_nonfatal_exception
e | Error.LspException
e) as exn
->
4814 let exn = Exception.wrap
exn in
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
;
4826 let exn = Exception.wrap
exn in
4829 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
4830 message = Exception.get_ctor_string
exn;
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
;
4839 let rec main_loop () : unit Lwt.t =
4840 let%lwt
() = process_next_event () in
4843 let%lwt
() = main_loop () in
4844 Lwt.return
Exit_status.No_error