2 * Copyright (c) 2015, Facebook, Inc.
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the "hack" directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
15 (* All hack-specific code relating to LSP goes in here. *)
17 (* The environment for hh_client with LSP *)
19 from
: string; (* The source where the client was spawned from, i.e. nuclide, vim, emacs, etc. *)
20 use_ffp_autocomplete
: bool; (* Flag to turn on the (experimental) FFP based autocomplete *)
23 (* This LSP server uses progress to indicate its lazy initialization: the *)
24 (* lifetime of each progress indicator starts with do_initialize and ends *)
25 (* when the hello message is received from hh_server. That will happen upon *)
26 (* first initialization, and also in case the persistent connection is lost *)
27 (* it will happen when it's subsequently regained. In any case, the lifetimes *)
28 (* of our progress notifications are non-overlapping, so we can use a single *)
29 (* constant id for all of them. *)
30 let progress_id_initialize = 1
31 (* Progress and action-required is also used to report hh_server's typecheck *)
32 (* status from ServerCommandTypes.busy_status: whether it's ready, or doing *)
33 (* a local typecheck, or doing a global typecheck, etc. Again the lifetimes *)
34 (* of these are non-overlapping so again we use constant ids. *)
35 let progress_id_server_status = 2
36 let action_id_server_status = 3
38 (************************************************************************)
39 (** Conversions - ad-hoc ones written as needed them, not systematic **)
40 (************************************************************************)
42 let url_scheme_regex = Str.regexp
"^\\([a-zA-Z][a-zA-Z0-9+.-]+\\):"
43 (* this requires schemes with 2+ characters, so "c:\path" isn't considered a scheme *)
45 let lsp_uri_to_path (uri
: string) : string =
46 if Str.string_match
url_scheme_regex uri
0 then
47 let scheme = Str.matched_group
1 uri
in
48 if scheme = "file" then
51 raise
(Error.InvalidParams
(Printf.sprintf
"Not a valid file url '%s'" uri
))
55 let path_to_lsp_uri (path
: string) ~
(default_path
: string): string =
56 if path
= "" then File_url.create default_path
57 else File_url.create path
59 let lsp_textDocumentIdentifier_to_filename
60 (identifier
: Lsp.TextDocumentIdentifier.t
)
62 let open Lsp.TextDocumentIdentifier
in
63 lsp_uri_to_path identifier
.uri
65 let lsp_position_to_ide (position
: Lsp.position
) : Ide_api_types.position
=
67 line
= position
.line
+ 1;
68 column
= position
.character
+ 1;
71 let lsp_file_position_to_hack (params
: Lsp.TextDocumentPositionParams.t
)
72 : string * int * int =
73 let open Lsp.TextDocumentPositionParams
in
74 let {Ide_api_types.line
; column
;} = lsp_position_to_ide params
.position
in
75 let filename = lsp_textDocumentIdentifier_to_filename params
.textDocument
77 (filename, line
, column
)
79 let hack_pos_to_lsp_range (pos
: 'a
Pos.pos
) : Lsp.range
=
80 let line1, col1
, line2
, col2
= Pos.destruct_range pos
in
82 start
= {line
= line1 - 1; character
= col1
- 1;};
83 end_
= {line
= line2
- 1; character
= col2
- 1;};
86 let hack_pos_to_lsp_location (pos
: string Pos.pos
) ~
(default_path
: string): Lsp.Location.t
=
87 let open Lsp.Location
in
89 uri
= path_to_lsp_uri (Pos.filename pos
) ~default_path
;
90 range
= hack_pos_to_lsp_range pos
;
93 let ide_range_to_lsp (range
: Ide_api_types.range
) : Lsp.range
=
96 line
= range
.Ide_api_types.st
.Ide_api_types.line
- 1;
97 character
= range
.Ide_api_types.st
.Ide_api_types.column
- 1;
100 line
= range
.Ide_api_types.ed
.Ide_api_types.line
- 1;
101 character
= range
.Ide_api_types.ed
.Ide_api_types.column
- 1;
105 let lsp_range_to_ide (range
: Lsp.range
) : Ide_api_types.range
=
106 let open Ide_api_types
in
108 st
= lsp_position_to_ide range
.start
;
109 ed
= lsp_position_to_ide range
.end_
;
112 let hack_symbol_definition_to_lsp_location
113 (symbol
: string SymbolDefinition.t
)
114 ~
(default_path
: string)
116 let open SymbolDefinition
in
117 hack_pos_to_lsp_location symbol
.pos ~default_path
119 let hack_errors_to_lsp_diagnostic
121 (errors
: Pos.absolute
Errors.error_ list
)
122 : PublishDiagnostics.params
=
123 let open Lsp.Location
in
124 let location_message (error
: Pos.absolute
* string) : (Lsp.Location.t
* string) =
125 let (pos
, message
) = error
in
126 let {uri
; range
;} = hack_pos_to_lsp_location pos ~default_path
:filename in
127 ({Location.uri
; range
;}, message
)
129 let hack_error_to_lsp_diagnostic (error
: Pos.absolute
Errors.error_
) =
130 let all_messages = Errors.to_list error
|> List.map ~f
:location_message in
131 let (first_message
, additional_messages
) = match all_messages with
132 | hd
:: tl
-> (hd
, tl
)
133 | [] -> failwith
"Expected at least one error in the error list"
135 let ({range
; _
}, message
) = first_message
in
136 let relatedLocations = additional_messages
|> List.map ~f
:(fun (location
, message
) ->
137 { PublishDiagnostics.
138 relatedLocation
= location
;
139 relatedMessage
= message
;
141 { Lsp.PublishDiagnostics.
143 severity
= Some
PublishDiagnostics.Error
;
144 code
= Some
(Errors.get_code error
);
145 source
= Some
"Hack";
150 (* The caller is required to give us a non-empty filename. If it is empty, *)
151 (* the following path_to_lsp_uri will fall back to the default path - which *)
152 (* is also empty - and throw, logging appropriate telemetry. *)
153 { Lsp.PublishDiagnostics.
154 uri
= path_to_lsp_uri filename ~default_path
:"";
155 diagnostics
= List.map errors ~f
:hack_error_to_lsp_diagnostic;
158 (************************************************************************)
159 (** Protocol orchestration & helpers **)
160 (************************************************************************)
163 ic
: Timeout.in_channel
;
166 (* Pending messages sent from the server. They need to be relayed to the
168 pending_messages
: ServerCommandTypes.push
Queue.t
;
171 module Main_env
= struct
175 uris_with_diagnostics
: SSet.t
;
176 ready_dialog_cancel
: (unit -> unit) option; (* "hack server is now ready" dialog *)
181 module In_init_env
= struct
185 busy_dialog_cancel
: (unit -> unit) option; (* "hack server is busy" dialog *)
186 file_edits
: Jsonrpc_queue.jsonrpc_message
ImmQueue.t
;
192 (* Pre_init: we haven't yet received the initialize request. *)
194 (* In_init: we did respond to the initialize request, and now we're *)
195 (* waiting for a "Hello" from the server. When that comes we'll *)
196 (* request a permanent connection from the server, and process the *)
197 (* file_changes backlog, and switch to Main_loop. *)
198 | In_init
of In_init_env.t
199 (* Main_loop: we have a working connection to both server and client. *)
200 | Main_loop
of Main_env.t
201 (* Lost_server: someone stole the persistent connection from us. *)
202 (* We might choose to grab it back if prompted... *)
204 (* Post_shutdown: we received a shutdown request from the client, and *)
205 (* therefore shut down our connection to the server. We can't handle *)
206 (* any more requests from the client and will close as soon as it *)
207 (* notifies us that we can exit. *)
210 let initialize_params: Initialize.params
option ref = ref None
214 | Server_message
of ServerCommandTypes.push
215 | Client_message
of Jsonrpc_queue.jsonrpc_message
216 | Tick
(* once per second, on idle *)
218 (* Here are some exit points. The "exit_fail_delay" is in case the user *)
219 (* restarted hh_server themselves: we'll give them a chance to start it up *)
220 (* rather than letting our client aggressively start it up first. *)
221 let exit_ok () = exit
0
222 let exit_fail () = exit
1
223 let exit_fail_delay () = Unix.sleep
2; exit
1
225 (* The following connection exceptions inform the main LSP event loop how to *)
226 (* respond to an exception: was the exception a connection-related exception *)
227 (* (one of these) or did it arise during other logic (not one of these)? Can *)
228 (* we report the exception to the LSP client? Can we continue handling *)
229 (* further LSP messages or must we quit? If we quit, can we do so immediately *)
230 (* or must we delay? -- Separately, they also help us marshal callstacks *)
231 (* across daemon- and process-boundaries. *)
232 exception Client_fatal_connection_exception
of Marshal_tools.remote_exception_data
233 exception Client_recoverable_connection_exception
of Marshal_tools.remote_exception_data
234 exception Server_fatal_connection_exception
of Marshal_tools.remote_exception_data
237 (* To handle requests, we use a global list of callbacks for when the *)
238 (* response is received, and a global id counter for correlation... *)
239 type on_result_callback
=
240 state
:state
-> result
:Hh_json.json
option -> state
242 type on_error_callback
=
243 state
:state
-> code
:int -> message
:string -> data
:Hh_json.json
option -> state
245 module Callback
= struct
248 on_result
: on_result_callback
;
249 on_error
: on_error_callback
;
253 let requests_counter: IMap.key
ref = ref 0
254 let requests_outstanding: Callback.t
IMap.t
ref = ref IMap.empty
257 let event_to_string (event
: event
) : string =
258 let open Jsonrpc_queue
in
260 | Server_hello
-> "Server hello"
261 | Server_message
ServerCommandTypes.DIAGNOSTIC _
-> "Server DIAGNOSTIC"
262 | Server_message
ServerCommandTypes.BUSY_STATUS _
-> "Server BUSY_STATUS"
263 | Server_message
ServerCommandTypes.NEW_CLIENT_CONNECTED
-> "Server NEW_CLIENT_CONNECTED"
264 | Server_message
ServerCommandTypes.FATAL_EXCEPTION _
-> "Server FATAL_EXCEPTION"
265 | Client_message c
-> Printf.sprintf
"Client %s %s" (kind_to_string c
.kind
) c
.method_
269 let state_to_string (state
: state
) : string =
271 | Pre_init
-> "Pre_init"
272 | In_init _ienv
-> "In_init"
273 | Main_loop _menv
-> "Main_loop"
274 | Lost_server
-> "Lost_server"
275 | Post_shutdown
-> "Post_shutdown"
278 let get_root () : Path.t
option =
279 let open Lsp.Initialize
in
280 match !initialize_params with
283 let path = match params
.rootUri
with
284 | Some uri
-> Some
(lsp_uri_to_path uri
)
285 | None
-> params
.rootPath
287 Some
(ClientArgsUtils.get_root path)
290 let supports_progress () : bool =
291 let open Lsp.Initialize
in
292 Option.value_map
!initialize_params
293 ~default
:false ~f
:(fun params
-> params
.client_capabilities
.window
.progress
)
296 let supports_actionRequired () : bool =
297 let open Lsp.Initialize
in
298 Option.value_map
!initialize_params
299 ~default
:false ~f
:(fun params
-> params
.client_capabilities
.window
.actionRequired
)
303 (server_conn
: server_conn
)
304 (command
: 'a
ServerCommandTypes.t
)
307 let res, pending_messages
=
308 ServerCommand.rpc_persistent
(server_conn
.ic
, server_conn
.oc
) command
in
309 List.iter pending_messages
310 ~f
:(fun x
-> Queue.push x server_conn
.pending_messages
);
313 | ServerCommand.Remote_exception remote_e_data
->
314 raise
(Server_fatal_connection_exception remote_e_data
)
316 let message = Printexc.to_string e
in
317 let stack = Printexc.get_backtrace
() in
318 raise
(Server_fatal_connection_exception
{ Marshal_tools.message; stack; })
321 (* respond: produces either a Response or an Error message, according
322 to whether the json has an error-code or not. Note that JsonRPC and LSP
323 mandate id to be present. *)
325 (outchan
: out_channel
)
326 (c
: Jsonrpc_queue.jsonrpc_message
)
328 : Hh_json.json
option =
329 let open Jsonrpc_queue
in
331 let is_error = (Jget.val_opt
(Some json
) "code" <> None
) in
332 let response = JSON_Object
(
333 ["jsonrpc", JSON_String
"2.0"]
335 ["id", match c
.id
with Some id
-> id
| None
-> JSON_Null
]
337 (if is_error then ["error", json
] else ["result", json
])
340 response |> Hh_json.json_to_string
|> Http_lite.write_message outchan
;
343 (* notify: produces a Notify message *)
344 let notify (outchan
: out_channel
) (method_
: string) (json
: Hh_json.json
)
347 let message = JSON_Object
[
348 "jsonrpc", JSON_String
"2.0";
349 "method", JSON_String method_
;
353 message |> Hh_json.json_to_string
|> Http_lite.write_message outchan
355 (* request: produce a Request message; returns a method you can call to cancel it *)
357 (outchan
: out_channel
)
358 (on_result
: on_result_callback
)
359 (on_error
: on_error_callback
)
363 incr
requests_counter;
364 let callback = { Callback.method_
; on_result
; on_error
; } in
365 let request_id = !requests_counter in
366 requests_outstanding := IMap.add
request_id callback !requests_outstanding;
369 let message = JSON_Object
[
370 "jsonrpc", string_
"2.0";
371 "id", int_
request_id;
372 "method", string_ method_
;
376 let cancel_message = JSON_Object
[
377 "jsonrpc", string_
"2.0";
378 "method", string_
"$/cancelRequest";
379 "params", JSON_Object
[
380 "id", int_
request_id;
384 message |> Hh_json.json_to_string
|> Http_lite.write_message outchan
;
386 let cancel () = cancel_message |> Hh_json.json_to_string
|> Http_lite.write_message outchan
390 let get_outstanding_request (id
: Hh_json.json
option) =
392 | Some
(Hh_json.JSON_Number s
) -> begin
394 let id = int_of_string s
in
395 Option.map
(IMap.get
id !requests_outstanding) ~f
:(fun v
-> (id, v
))
396 with Failure _
-> None
400 let get_outstanding_method_name (id: Hh_json.json
option) : string =
402 match (get_outstanding_request id) with
403 | Some
(_
, callback) -> callback.method_
408 (id: Hh_json.json
option)
409 (result
: Hh_json.json
option)
410 (error
: Hh_json.json
option)
413 let id, on_result
, on_error
= match (get_outstanding_request id) with
414 | Some
(id, callback) -> (id, callback.on_result
, callback.on_error
)
415 | None
-> raise
(Error.InvalidRequest
"response to non-existent id")
417 requests_outstanding := IMap.remove
id !requests_outstanding;
418 if Option.is_some error
then
419 let code = Jget.int_exn error
"code" in
420 let message = Jget.string_exn error
"message" in
421 let data = Jget.val_opt error
"data" in
422 on_error state
code message data
424 on_result state result
427 let client_log (level
: Lsp.MessageType.t
) (message: string) : unit =
428 print_logMessage level
message |> notify stdout
"telemetry/event"
431 (event
: event
option)
435 (start_handle_t
: float)
437 let root = get_root () in
439 | Some Client_message c
->
440 let open Jsonrpc_queue
in
441 HackEventLogger.client_lsp_method_exception
442 root c
.method_
(kind_to_string c
.kind
) c
.timestamp start_handle_t c
.message_json_for_logging
445 HackEventLogger.client_lsp_exception
root message stack source
448 (* Determine whether to read a message from the client (the editor) or the
449 server (hh_server), or whether neither is ready within 1s. *)
450 let get_message_source
451 (server
: server_conn
)
452 (client
: Jsonrpc_queue.t
)
453 : [> `From_server
| `From_client
| `No_source
] =
454 (* Take action on server messages in preference to client messages, because
455 server messages are very easy and quick to service (just send a message to
456 the client), while client messages require us to launch a potentially
457 long-running RPC command. *)
458 let has_server_messages = not
(Queue.is_empty server
.pending_messages
) in
459 if has_server_messages then `From_server
else
460 if Jsonrpc_queue.has_message client
then `From_client
else
462 (* If no immediate messages are available, then wait up to 1 second. *)
463 let server_read_fd = Unix.descr_of_out_channel server
.oc
in
464 let client_read_fd = Jsonrpc_queue.get_read_fd client
in
465 let readable, _
, _
= Unix.select
[server_read_fd; client_read_fd] [] [] 1.0 in
466 if readable = [] then `No_source
467 else if List.mem
readable server_read_fd then `From_server
471 (* A simplified version of get_message_source which only looks at client *)
472 let get_client_message_source
473 (client
: Jsonrpc_queue.t
)
474 : [> `From_client
| `No_source
] =
475 if Jsonrpc_queue.has_message client
then `From_client
else
476 let client_read_fd = Jsonrpc_queue.get_read_fd client
in
477 let readable, _
, _
= Unix.select
[client_read_fd] [] [] 1.0 in
478 if readable = [] then `No_source
482 (* Read a message unmarshaled from the server's out_channel. *)
483 let read_message_from_server (server
: server_conn
) : event
=
484 let open ServerCommandTypes
in
486 let fd = Unix.descr_of_out_channel server
.oc
in
487 match Marshal_tools.from_fd_with_preamble
fd with
489 failwith
"unexpected response without request"
490 | Push m
-> Server_message m
491 | Hello
-> Server_hello
493 let message = Printexc.to_string e
in
494 let stack = Printexc.get_backtrace
() in
495 raise
(Server_fatal_connection_exception
{ Marshal_tools.message; stack; })
497 (* get_next_event: picks up the next available message from either client or
498 server. The way it's implemented, at the first character of a message
499 from either client or server, we block until that message is completely
500 received. Note: if server is None (meaning we haven't yet established
501 connection with server) then we'll just block waiting for client. *)
502 let get_next_event (state
: state
) (client
: Jsonrpc_queue.t
) : event
=
503 let from_server (server
: server_conn
) =
504 if Queue.is_empty server
.pending_messages
505 then read_message_from_server server
506 else Server_message
(Queue.take server
.pending_messages
)
509 let from_client (client
: Jsonrpc_queue.t
) =
510 match Jsonrpc_queue.get_message client
with
511 | Jsonrpc_queue.Message
message -> Client_message
message
512 | Jsonrpc_queue.Fatal_exception edata
->
513 raise
(Client_fatal_connection_exception edata
)
514 | Jsonrpc_queue.Recoverable_exception edata
->
515 raise
(Client_recoverable_connection_exception edata
)
519 | Main_loop
{ Main_env.conn
; _
} | In_init
{ In_init_env.conn
; _
} -> begin
520 match get_message_source conn client
with
521 | `From_client
-> from_client client
522 | `From_server
-> from_server conn
526 match get_client_message_source client
with
527 | `From_client
-> from_client client
533 (* cancel_if_stale: If a message is stale, throw the necessary exception to
534 cancel it. A message is considered stale if it's sufficiently old and there
535 are other messages in the queue that are newer than it. *)
536 let short_timeout = 2.5
537 let long_timeout = 15.0
540 (client
: Jsonrpc_queue.t
)
541 (message: Jsonrpc_queue.jsonrpc_message
)
544 let message_received_time = message.Jsonrpc_queue.timestamp
in
545 let time_elapsed = (Unix.gettimeofday
()) -. message_received_time in
546 if time_elapsed >= timeout
&& Jsonrpc_queue.has_message client
547 then raise
(Error.RequestCancelled
"request timed out")
550 (* respond_to_error: if we threw an exception during the handling of a request,
551 report the exception to the client as the response to their request. *)
552 let respond_to_error (event
: event
option) (e
: exn
) (stack: string): unit =
554 | Some
(Client_message c
)
555 when c
.Jsonrpc_queue.kind
= Jsonrpc_queue.Request
->
556 print_error e
stack |> respond stdout c
|> ignore
560 (************************************************************************)
562 (************************************************************************)
564 let do_shutdown (conn
: server_conn
) : Shutdown.result
=
565 rpc conn
(ServerCommandTypes.UNSUBSCRIBE_DIAGNOSTIC
0);
566 rpc conn
(ServerCommandTypes.DISCONNECT
);
569 let do_didOpen (conn
: server_conn
) (params
: DidOpen.params
) : unit =
571 let open TextDocumentItem
in
572 let filename = lsp_uri_to_path params
.textDocument
.uri
in
573 let text = params
.textDocument
.text in
574 let command = ServerCommandTypes.OPEN_FILE
(filename, text) in
578 let do_didClose (conn
: server_conn
) (params
: DidClose.params
) : unit =
580 let open TextDocumentIdentifier
in
581 let filename = lsp_uri_to_path params
.textDocument
.uri
in
582 let command = ServerCommandTypes.CLOSE_FILE
filename in
588 (params
: DidChange.params
)
590 let open VersionedTextDocumentIdentifier
in
591 let open Lsp.DidChange
in
592 let lsp_change_to_ide (lsp
: DidChange.textDocumentContentChangeEvent
)
593 : Ide_api_types.text_edit
=
595 range
= Option.map lsp
.range
lsp_range_to_ide;
599 let filename = lsp_uri_to_path params
.textDocument
.uri
in
600 let changes = List.map params
.contentChanges ~f
:lsp_change_to_ide in
601 let command = ServerCommandTypes.EDIT_FILE
(filename, changes) in
605 let do_hover (conn
: server_conn
) (params
: Hover.params
) : Hover.result
=
606 (* TODO: should return MarkedCode, once Nuclide supports it *)
607 (* TODO: should return doc-comment as well *)
608 (* TODO: should return signature of what we hovered on, not just type. *)
609 let (file
, line
, column
) = lsp_file_position_to_hack params
in
610 let command = ServerCommandTypes.INFER_TYPE
(ServerUtils.FileName file
, line
, column
) in
611 let inferred_type = rpc conn
command in
612 match inferred_type with
613 (* Hack server uses both None and "_" to indicate absence of a result. *)
614 (* We're also catching the non-result "" just in case... *)
617 | Some
("", _
) -> { Hover.contents
= []; range
= None
; }
618 | Some
(s
, _
) -> { Hover.contents
= [MarkedString s
]; range
= None
; }
620 let do_definition (conn
: server_conn
) (params
: Definition.params
)
621 : Definition.result
=
622 let (file
, line
, column
) = lsp_file_position_to_hack params
in
623 let command = ServerCommandTypes.IDENTIFY_FUNCTION
(ServerUtils.FileName file
, line
, column
) in
624 let results = rpc conn
command in
625 (* What's it like when we return multiple definitions? For instance, if you ask *)
626 (* for the definition of "new C()" then we've now got the definition of the *)
627 (* class "\C" and also of the constructor "\\C::__construct". I think that *)
628 (* users would be happier to only have the definition of the constructor, so *)
629 (* as to jump straight to it without the fuss of clicking to select which one. *)
630 (* That indeed is what Typescript does -- it only gives the constructor. *)
631 (* (VSCode displays multiple definitions with a peek view of them all; *)
632 (* Atom displays them with a small popup showing just file+line of each). *)
633 (* There's one subtlety. If you declare a base class "B" with a constructor, *)
634 (* and a derived class "C" without a constructor, and click on "new C()", then *)
635 (* both Hack and Typescript will take you to the constructor of B. As desired! *)
636 (* Conclusion: given a class+method, we'll return only the method. *)
637 let result_is (kind
: SymbolDefinition.kind
) (result
: IdentifySymbolService.single_result
): bool =
640 | (_
, Some definition
) -> definition
.SymbolDefinition.kind
= kind
642 let has_class = List.exists
results ~f
:(result_is SymbolDefinition.Class
) in
643 let has_method = List.exists
results ~f
:(result_is SymbolDefinition.Method
) in
644 let filtered_results = if has_class && has_method then
645 List.filter
results ~f
:(result_is SymbolDefinition.Method
)
649 let rec hack_to_lsp = function
651 | (_occurrence
, None
) :: l
-> hack_to_lsp l
652 | (_occurrence
, Some definition
) :: l
->
653 (hack_symbol_definition_to_lsp_location definition ~default_path
:file
) :: (hack_to_lsp l
)
655 hack_to_lsp filtered_results
657 let make_ide_completion_response (result
:AutocompleteTypes.ide_result
) =
658 let open AutocompleteTypes
in
659 let open Completion
in
660 let open Initialize
in
661 (* We use snippets to provide parentheses+arguments when autocompleting *)
662 (* method calls e.g. "$c->|" ==> "$c->foo($arg1)". But we'll only do this *)
663 (* there's nothing after the caret: no "$c->|(1)" -> "$c->foo($arg1)(1)" *)
664 let is_caret_followed_by_whitespace = result
.char_at_pos
= ' '
|| result
.char_at_pos
= '
\n'
in
665 let client_supports_snippets = Option.value_map
!initialize_params
666 ~default
:false ~f
:(fun params
->
667 params
.client_capabilities
.textDocument
.completion
.completionItem
.snippetSupport
) in
669 let rec hack_completion_to_lsp (completion
: complete_autocomplete_result
)
670 : Completion.completionItem
=
671 let (insertText
, insertTextFormat
) = hack_to_insert completion
in
673 label
= completion
.res_name ^
(if completion
.res_kind
= Namespace_kind
then "\\" else "");
674 kind
= hack_to_kind completion
;
675 detail
= Some
(hack_to_detail completion
);
676 inlineDetail
= Some
(hack_to_inline_detail completion
);
677 itemType
= hack_to_itemType completion
;
678 documentation
= None
; (* TODO: provide doc-comments *)
681 insertText
= Some insertText
;
682 insertTextFormat
= insertTextFormat
;
687 and hack_to_kind
(completion
: complete_autocomplete_result
)
688 : Completion.completionItemKind
option =
689 match completion
.res_kind
with
690 | Abstract_class_kind
691 | Class_kind
-> Some
Completion.Class
692 | Method_kind
-> Some
Completion.Method
693 | Function_kind
-> Some
Completion.Function
694 | Variable_kind
-> Some
Completion.Variable
695 | Property_kind
-> Some
Completion.Property
696 | Class_constant_kind
-> Some
Completion.Value
(* a bit off, but the best we can do *)
698 | Trait_kind
-> Some
Completion.Interface
699 | Enum_kind
-> Some
Completion.Enum
700 | Namespace_kind
-> Some
Completion.Module
701 | Constructor_kind
-> Some
Completion.Constructor
702 | Keyword_kind
-> Some
Completion.Keyword
703 and hack_to_itemType
(completion
: complete_autocomplete_result
) : string option =
704 (* TODO: we're using itemType (left column) for function return types, and *)
705 (* the inlineDetail (right column) for variable/field types. Is that good? *)
706 Option.map completion
.func_details ~f
:(fun details
-> details
.return_ty
)
707 and hack_to_detail
(completion
: complete_autocomplete_result
) : string =
708 (* TODO: retrieve the actual signature including name+modifiers *)
709 (* For now we just return the type of the completion. In the case *)
710 (* of functions, their function-types have parentheses around them *)
711 (* which we want to strip. In other cases like tuples, no strip. *)
712 match completion
.func_details
with
713 | None
-> completion
.res_ty
714 | Some _
-> String_utils.rstrip
(String_utils.lstrip completion
.res_ty
"(") ")"
715 and hack_to_inline_detail
(completion
: complete_autocomplete_result
) : string =
716 match completion
.func_details
with
717 | None
-> hack_to_detail completion
719 (* "(type1 $param1, ...)" *)
720 let f param
= Printf.sprintf
"%s %s" param
.param_ty param
.param_name
in
721 let params = String.concat
", " (List.map details
.params ~
f) in
722 Printf.sprintf
"(%s)" params
723 and hack_to_insert
(completion
: complete_autocomplete_result
) : (string * insertTextFormat
) =
724 match completion
.func_details
with
725 | Some details
when is_caret_followed_by_whitespace && client_supports_snippets ->
726 (* "method(${1:arg1}, ...)" but for args we just use param names. *)
727 let f i param
= Printf.sprintf
"${%i:%s}" (i
+ 1) param
.param_name
in
728 let params = String.concat
", " (List.mapi details
.params ~
f) in
729 (Printf.sprintf
"%s(%s)" completion
.res_name
params, SnippetFormat
)
731 (completion
.res_name
, PlainText
)
734 isIncomplete
= not result
.is_complete
;
735 items
= List.map result
.completions ~
f:hack_completion_to_lsp;
738 let do_completion_ffp (conn
: server_conn
) (params: Completion.params) : Completion.result
=
739 let open TextDocumentIdentifier
in
740 let pos = lsp_position_to_ide params.TextDocumentPositionParams.position
in
741 let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument
.uri
in
742 let command = ServerCommandTypes.IDE_FFP_AUTOCOMPLETE
(filename, pos) in
743 let result = rpc conn
command in
744 make_ide_completion_response result
746 let do_completion_legacy (conn
: server_conn
) (params: Completion.params)
747 : Completion.result =
748 let open TextDocumentIdentifier
in
749 let pos = lsp_position_to_ide params.TextDocumentPositionParams.position
in
750 let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument
.uri
in
751 let delimit_on_namespaces = true in
752 let command = ServerCommandTypes.IDE_AUTOCOMPLETE
(filename, pos, delimit_on_namespaces) in
753 let result = rpc conn
command in
754 make_ide_completion_response result
756 let do_workspaceSymbol
758 (params: WorkspaceSymbol.params)
759 : WorkspaceSymbol.result =
760 let open WorkspaceSymbol
in
761 let open SearchUtils
in
763 let query = params.query in
764 let query_type = "" in
765 let command = ServerCommandTypes.SEARCH
(query, query_type) in
766 let results = rpc conn
command in
768 let hack_to_lsp_kind = function
769 | HackSearchService.Class
(Some
Ast.Cabstract
) -> SymbolInformation.Class
770 | HackSearchService.Class
(Some
Ast.Cnormal
) -> SymbolInformation.Class
771 | HackSearchService.Class
(Some
Ast.Cinterface
) -> SymbolInformation.Interface
772 | HackSearchService.Class
(Some
Ast.Ctrait
) -> SymbolInformation.Interface
773 (* LSP doesn't have traits, so we approximate with interface *)
774 | HackSearchService.Class
(Some
Ast.Cenum
) -> SymbolInformation.Enum
775 | HackSearchService.Class
(None
) -> assert false (* should never happen *)
776 | HackSearchService.Method _
-> SymbolInformation.Method
777 | HackSearchService.ClassVar _
-> SymbolInformation.Property
778 | HackSearchService.Function
-> SymbolInformation.Function
779 | HackSearchService.Typedef
-> SymbolInformation.Class
780 (* LSP doesn't have typedef, so we approximate with class *)
781 | HackSearchService.Constant
-> SymbolInformation.Constant
783 let hack_to_lsp_container = function
784 | HackSearchService.Method
(_
, scope
) -> Some scope
785 | HackSearchService.ClassVar
(_
, scope
) -> Some scope
788 (* Hack sometimes gives us back items with an empty path, by which it *)
789 (* intends "whichever path you asked me about". That would be meaningless *)
790 (* here. If it does, then it'll pick up our default path (also empty), *)
791 (* which will throw and go into our telemetry. That's the best we can do. *)
792 let hack_symbol_to_lsp (symbol
: HackSearchService.symbol
) =
794 name
= (Utils.strip_ns symbol
.name
);
795 kind
= hack_to_lsp_kind symbol
.result_type
;
796 location
= hack_pos_to_lsp_location symbol
.pos ~default_path
:"";
797 containerName
= hack_to_lsp_container symbol
.result_type
;
800 List.map
results ~
f:hack_symbol_to_lsp
802 let do_documentSymbol
804 (params: DocumentSymbol.params)
805 : DocumentSymbol.result =
806 let open DocumentSymbol
in
807 let open TextDocumentIdentifier
in
808 let open SymbolDefinition
in
810 let filename = lsp_uri_to_path params.textDocument
.uri
in
811 let command = ServerCommandTypes.OUTLINE
filename in
812 let results = rpc conn
command in
814 let hack_to_lsp_kind = function
815 | SymbolDefinition.Function
-> SymbolInformation.Function
816 | SymbolDefinition.Class
-> SymbolInformation.Class
817 | SymbolDefinition.Method
-> SymbolInformation.Method
818 | SymbolDefinition.Property
-> SymbolInformation.Property
819 | SymbolDefinition.Const
-> SymbolInformation.Constant
820 | SymbolDefinition.Enum
-> SymbolInformation.Enum
821 | SymbolDefinition.Interface
-> SymbolInformation.Interface
822 | SymbolDefinition.Trait
-> SymbolInformation.Interface
823 (* LSP doesn't have traits, so we approximate with interface *)
824 | SymbolDefinition.LocalVar
-> SymbolInformation.Variable
825 | SymbolDefinition.Typeconst
-> SymbolInformation.Class
826 (* e.g. "const type Ta = string;" -- absent from LSP *)
827 | SymbolDefinition.Typedef
-> SymbolInformation.Class
828 (* e.g. top level type alias -- absent from LSP *)
829 | SymbolDefinition.Param
-> SymbolInformation.Variable
830 (* We never return a param from a document-symbol-search *)
832 let hack_symbol_to_lsp definition containerName
=
834 name
= definition
.name
;
835 kind
= hack_to_lsp_kind definition
.kind
;
836 location
= hack_symbol_definition_to_lsp_location definition ~default_path
:filename;
840 let rec hack_symbol_tree_to_lsp ~accu ~container_name
= function
841 (* Flattens the recursive list of symbols *)
842 | [] -> List.rev accu
844 let children = Option.value def
.children ~default
:[] in
845 let accu = (hack_symbol_to_lsp def container_name
) :: accu in
846 let accu = hack_symbol_tree_to_lsp accu (Some def
.name
) children in
847 hack_symbol_tree_to_lsp accu container_name defs
849 hack_symbol_tree_to_lsp ~
accu:[] ~container_name
:None
results
851 let do_findReferences
853 (params: FindReferences.params)
854 : FindReferences.result =
855 let open FindReferences
in
857 let {Ide_api_types.line
; column
;} = lsp_position_to_ide params.position
in
858 let filename = lsp_textDocumentIdentifier_to_filename params.textDocument
in
859 let include_defs = params.context
.includeDeclaration
in
860 let command = ServerCommandTypes.IDE_FIND_REFS
861 (ServerUtils.FileName
filename, line
, column
, include_defs) in
862 let results = rpc conn
command in
863 (* TODO: respect params.context.include_declaration *)
866 | Some
(_name
, positions
) ->
867 List.map positions ~
f:(hack_pos_to_lsp_location ~default_path
:filename)
870 let do_documentHighlights
872 (params: DocumentHighlights.params)
873 : DocumentHighlights.result =
874 let open DocumentHighlights
in
876 let (file
, line
, column
) = lsp_file_position_to_hack params in
877 let command = ServerCommandTypes.IDE_HIGHLIGHT_REFS
(ServerUtils.FileName file
, line
, column
) in
878 let results = rpc conn
command in
880 let hack_range_to_lsp_highlight range
=
882 range
= ide_range_to_lsp range
;
886 List.map
results ~
f:hack_range_to_lsp_highlight
889 let do_typeCoverage (conn
: server_conn
) (params: TypeCoverage.params)
890 : TypeCoverage.result =
891 let open TypeCoverage
in
893 let filename = lsp_textDocumentIdentifier_to_filename params.textDocument
in
894 let command = ServerCommandTypes.COVERAGE_LEVELS
(ServerUtils.FileName
filename) in
895 let results: Coverage_level.result = rpc conn
command in
896 let results = Coverage_level.merge_adjacent_results
results in
898 (* We want to get a percentage-covered number. We could do that with an *)
899 (* additional server round trip to ServerCommandTypes.COVERAGE_COUNTS. *)
900 (* But to avoid that, we'll instead use this rough approximation: *)
901 (* Count how many checked/unchecked/partial "regions" there are, where *)
902 (* a "region" is like results_merged, but counting each line separately. *)
903 let count_region (nchecked
, nunchecked
, npartial
) (pos, level
) =
904 let nlines = (Pos.end_line
pos) - (Pos.line
pos) + 1 in
906 | Ide_api_types.Checked
-> (nchecked
+ nlines, nunchecked
, npartial
)
907 | Ide_api_types.Unchecked
-> (nchecked
, nunchecked
+ nlines, npartial
)
908 | Ide_api_types.Partial
-> (nchecked
, nunchecked
, npartial
+ nlines)
910 let (nchecked
, nunchecked
, npartial
) =
911 List.fold
results ~init
:(0,0,0) ~
f:count_region in
913 let ntotal = nchecked
+ nunchecked
+ npartial
in
914 let coveredPercent = if ntotal = 0 then 100
915 else ((nchecked
* 100) + (npartial
* 50)) / ntotal in
917 let hack_coverage_to_lsp (pos, level
) =
918 let range = hack_pos_to_lsp_range pos in
920 | Ide_api_types.Checked
-> None
921 | Ide_api_types.Unchecked
-> Some
923 message = "Un-type checked code. Consider adding type annotations.";
925 | Ide_api_types.Partial
-> Some
927 message = "Partially type checked code. Consider adding type annotations.";
932 uncoveredRanges
= List.filter_map
results ~
f:hack_coverage_to_lsp;
936 let do_formatting_common
938 (args
: ServerFormatTypes.ide_action
)
940 let open ServerFormatTypes
in
941 let command = ServerCommandTypes.IDE_FORMAT args
in
942 let response: ServerFormatTypes.ide_result
= rpc conn
command in
944 | Result.Error
message ->
945 raise
(Error.InternalError
message)
947 let range = ide_range_to_lsp r
.range in
948 let newText = r
.new_text
in
949 [{TextEdit.range; newText;}]
952 let do_documentRangeFormatting
954 (params: DocumentRangeFormatting.params)
955 : DocumentRangeFormatting.result =
956 let open DocumentRangeFormatting
in
957 let open TextDocumentIdentifier
in
958 let action = ServerFormatTypes.Range
960 range_filename
= lsp_uri_to_path params.textDocument
.uri
;
961 file_range
= lsp_range_to_ide params.range;
964 do_formatting_common conn
action
967 let do_documentOnTypeFormatting
969 (params: DocumentOnTypeFormatting.params)
970 : DocumentOnTypeFormatting.result =
971 let open DocumentOnTypeFormatting
in
972 let open TextDocumentIdentifier
in
973 let action = ServerFormatTypes.Position
975 filename = lsp_uri_to_path params.textDocument
.uri
;
976 position
= lsp_position_to_ide params.position
;
978 do_formatting_common conn
action
981 let do_documentFormatting
983 (params: DocumentFormatting.params)
984 : DocumentFormatting.result =
985 let open DocumentFormatting
in
986 let open TextDocumentIdentifier
in
987 let action = ServerFormatTypes.Document
(lsp_uri_to_path params.textDocument
.uri
) in
988 do_formatting_common conn
action
991 (* do_server_busy: controls the progress / action-required indicator *)
992 let do_server_busy (status
: ServerCommandTypes.busy_status
) : unit =
993 let open ServerCommandTypes
in
994 let (progress
, action) = match status
with
995 | Needs_local_typecheck
-> (Some
"Hack: preparing to check edits", None
)
996 | Doing_local_typecheck
-> (Some
"Hack: checking edits", None
)
997 | Done_local_typecheck
-> (None
, Some
"Hack: save any file to do a whole-program check")
998 | Doing_global_typecheck
-> (Some
"Hack: checking entire project", None
)
999 | Done_global_typecheck
-> (None
, None
)
1001 print_progress
progress_id_server_status progress
|> notify stdout
"window/progress";
1002 print_actionRequired
action_id_server_status action |> notify stdout
"window/actionRequired";
1006 (* do_diagnostics: sends notifications for all reported diagnostics; also *)
1007 (* returns an updated "files_with_diagnostics" set of all files for which *)
1008 (* our client currently has non-empty diagnostic reports. *)
1010 (uris_with_diagnostics
: SSet.t
)
1011 (file_reports
: Pos.absolute
Errors.error_ list
SMap.t
)
1013 (* Hack sometimes reports a diagnostic on an empty file when it can't *)
1014 (* figure out which file to report. In this case we'll report on the root. *)
1015 (* Nuclide and VSCode both display this fine, though they obviously don't *)
1016 (* let you click-to-go-to-file on it. *)
1017 let default_path = match get_root () with
1018 | None
-> failwith
"expected root"
1019 | Some
root -> Path.to_string
root in
1020 let file_reports = match SMap.get
"" file_reports with
1021 | None
-> file_reports
1022 | Some errors
-> SMap.remove
"" file_reports |> SMap.add ~combine
:(@) default_path errors
1025 let per_file file errors
=
1026 hack_errors_to_lsp_diagnostic file errors
1027 |> print_diagnostics
1028 |> notify stdout
"textDocument/publishDiagnostics"
1030 SMap.iter
per_file file_reports;
1032 let is_error_free _uri errors
= List.is_empty errors
in
1033 (* reports_without/reports_with are maps of filename->ErrorList. *)
1034 let (reports_without
, reports_with
) = SMap.partition
is_error_free file_reports in
1035 (* files_without/files_with are sets of filenames *)
1036 let files_without = SMap.bindings reports_without
|> List.map ~
f:fst
in
1037 let files_with = SMap.bindings reports_with
|> List.map ~
f:fst
in
1038 (* uris_without/uris_with are sets of uris *)
1039 let uris_without = List.map
files_without ~
f:(path_to_lsp_uri ~
default_path) |> SSet.of_list
in
1040 let uris_with = List.map
files_with ~
f:(path_to_lsp_uri ~
default_path) |> SSet.of_list
1042 (* this is "(uris_with_diagnostics \ uris_without) U uris_with" *)
1043 SSet.union
(SSet.diff uris_with_diagnostics
uris_without) uris_with
1046 (* do_diagnostics_flush: sends out "no more diagnostics for these files" *)
1047 let do_diagnostics_flush
1048 (diagnostic_uris
: SSet.t
)
1051 { Lsp.PublishDiagnostics.uri
= uri
;
1054 |> print_diagnostics
1055 |> notify stdout
"textDocument/publishDiagnostics"
1057 SSet.iter
per_uri diagnostic_uris
1061 (ienv
: In_init_env.t
)
1063 (* Our goal behind progress reporting is to let the user know when things *)
1064 (* won't be instantaneous, and to show that things are working as expected. *)
1065 (* We already eagerly showed a "please wait" dialog box as soon as we gave *)
1066 (* do_initialize learned that it hadn't gotten a "hello" back immediately *)
1067 (* from the server. This report_progress is called once a second and will *)
1068 (* update the busy-tooltip. And once we do get the "hello", either close *)
1069 (* the busy indicator (if the client supports one), or log to the console *)
1070 (* (if the client doesn't), or show a completion dialog box (if we'd taken *)
1071 (* 30 seconds or more). *)
1072 let open In_init_env
in
1073 if supports_progress () then begin
1074 let time = Unix.time () in
1075 let delay_in_secs = int_of_float
(time -. ienv
.start_time
) in
1076 (* TODO: better to report time that hh_server has spent initializing *)
1077 let load_state_not_found, tail_msg
=
1078 ClientConnect.open_and_get_tail_msg ienv
.start_time ienv
.tail_env
in
1079 let msg = if load_state_not_found then
1081 "hh_server initializing (load-state not found - will take a while): %s [%i seconds]"
1082 tail_msg
delay_in_secs
1085 "hh_server initializing: %s [%i seconds]"
1086 tail_msg
delay_in_secs
1088 print_progress
progress_id_initialize (Some
msg) |> notify stdout
"window/progress"
1092 let report_progress_end
1093 (ienv
: In_init_env.t
)
1095 let open In_init_env
in
1098 conn
= ienv
.In_init_env.conn
;
1100 uris_with_diagnostics
= SSet.empty
;
1101 ready_dialog_cancel
= None
;
1104 (* dismiss the "busy..." dialog if present *)
1105 Option.call ~
f:ienv
.busy_dialog_cancel
();
1106 (* dismiss the "hh_server initializing" spinner if present *)
1107 if supports_progress () then
1108 print_progress
progress_id_initialize None
|> notify stdout
"window/progress";
1109 (* and alert the user that hack is ready, either by console log or by dialog *)
1110 let time = Unix.time () in
1111 let seconds = int_of_float
(time -. ienv
.start_time
) in
1112 let msg = Printf.sprintf
"hh_server is now ready, after %i seconds." seconds in
1113 if (time -. ienv
.start_time
> 30.0) then begin
1114 let clear_cancel_flag state
= match state
with
1115 | Main_loop
menv -> Main_loop
{menv with ready_dialog_cancel
= None
}
1118 let handle_result ~state ~
result:_
= clear_cancel_flag state
in
1119 let handle_error ~state ~
code:_ ~
message:_ ~
data:_
= clear_cancel_flag state
in
1120 let req = print_showMessageRequest
MessageType.InfoMessage
msg [] in
1121 let cancel = request stdout
handle_result handle_error "window/showMessageRequest" req in
1122 Main_loop
{menv with ready_dialog_cancel
= Some
cancel;}
1123 end else if (not
(supports_progress ())) then begin
1124 print_logMessage
MessageType.InfoMessage
msg |> notify stdout
"window/logMessage";
1130 (* After the server has sent 'hello', it means the persistent connection is *)
1131 (* ready, so we can send our backlog of file-edits to the server. *)
1132 let connect_after_hello
1133 (server_conn
: server_conn
)
1134 (file_edits
: Jsonrpc_queue.jsonrpc_message
ImmQueue.t
)
1136 let open Marshal_tools
in
1138 let oc = server_conn
.oc in
1139 ServerCommand.send_connection_type
oc ServerCommandTypes.Persistent
;
1140 let fd = Unix.descr_of_out_channel
oc in
1141 let response = Marshal_tools.from_fd_with_preamble
fd in
1142 if response <> ServerCommandTypes.Connected
then
1143 failwith
"Didn't get server Connected response";
1145 let handle_file_edit (c
: Jsonrpc_queue.jsonrpc_message
) =
1146 let open Jsonrpc_queue
in
1147 match c
.method_
with
1148 | "textDocument/didOpen" -> parse_didOpen c
.params |> do_didOpen server_conn
1149 | "textDocument/didChange" -> parse_didChange c
.params |> do_didChange server_conn
1150 | "textDocument/didClose" -> parse_didClose c
.params |> do_didClose server_conn
1151 | _
-> failwith
"should only buffer up didOpen/didChange/didClose"
1153 ImmQueue.iter file_edits ~
f:handle_file_edit;
1155 let message = Printexc.to_string e
in
1156 let stack = Printexc.get_backtrace
() in
1157 raise
(Server_fatal_connection_exception
{ message; stack; })
1160 rpc server_conn
(ServerCommandTypes.SUBSCRIBE_DIAGNOSTIC
0)
1166 let open Exit_status
in
1167 let open Lsp.Error
in
1168 (* This basically does the same connection attempt as "hh_client check": *)
1169 (* it makes repeated attempts to connect; it prints useful messages to *)
1170 (* stderr; in case of failure it will raise an exception. Below we're *)
1171 (* catching the main exceptions so we can give a good user-facing error *)
1172 (* text. For other exceptions, they'll end up showing to the user just *)
1173 (* "internal error" with the error code. *)
1178 force_dormant_start
= false;
1179 retries
= Some
3; (* each retry takes up to 1 second *)
1180 expiry
= None
; (* we can limit retries by time as well as by count *)
1182 profile_log
= false; (* irrelevant *)
1184 progress_callback
= ClientConnect.null_progress_reporter
; (* we're fast! *)
1185 do_post_handoff_handshake
= false;
1188 let (ic
, oc) = ClientConnect.connect
env_connect in
1189 let pending_messages = Queue.create
() in
1190 { ic
; oc; pending_messages; }
1192 | Exit_with No_server_running
->
1193 (* Raised when (1) the connection was refused/timed-out and no lockfile *)
1194 (* is present; (2)) server was dormant and had already received too *)
1195 (* many pending connection requests. In all cases more detail has *)
1196 (* been printed to stderr. *)
1197 (* How should the user react? -- they can read the console to find out *)
1198 (* more; they can try to restart at the command-line; or they can try *)
1199 (* to restart within their editor. *)
1200 raise
(ServerErrorStart
(
1201 "Attempts to start Hack server have failed; see console for details.",
1202 { Lsp.Initialize.retry
= true; }))
1203 | Exit_with Out_of_retries
1204 | Exit_with Out_of_time
->
1205 (* Raised when we couldn't complete the entire handshake despite *)
1206 (* repeated attempts. Most likely because hh_server was busy loading *)
1207 (* saved-state, or failed to load saved-state and had to initialize *)
1208 (* everything itself. *)
1209 (* How should the user react? -- as above, by reading the console, by *)
1210 (* attempting restart at the command-line or in editor. *)
1211 raise
(ServerErrorStart
(
1212 "The Hack server is busy and unable to provide language services.",
1213 { Lsp.Initialize.retry
= true; }))
1216 let connect_attempt_hello
1217 (server_conn
: server_conn
)
1219 (* This waits up to 3 seconds for the server to send "Hello", the first *)
1220 (* message it sends after handoff. It might take some time if the server *)
1221 (* has to finish typechecking first. Returns whether it got the "Hello". *)
1223 let retries = Some
3 in (* it does one retry per second *)
1224 let tail_env = None
in (* don't report progress to stderr *)
1225 let time = Unix.time () in (* dummy; not used because of tail_env=None *)
1226 ClientConnect.wait_for_server_hello server_conn
.ic
retries
1227 ClientConnect.null_progress_reporter
time tail_env;
1230 | Exit_status.Exit_with
Exit_status.Out_of_retries
->
1232 | ClientConnect.Server_hung_up
->
1233 (* Raised by wait_for_server_hello, if someone killed the server while *)
1234 (* it was busy doing its typechecking or other work. *)
1235 (* How should user react? - by attempting to re-connect. *)
1236 raise
(Lsp.Error.ServerErrorStart
(
1237 "hh_server died unexpectedly. Maybe you recently launched a different version of hh_server.",
1238 { Lsp.Initialize.retry
= true; }))
1241 (* connect: this method attempts to connect to the server. If it can within *)
1242 (* three seconds then it returns Main_loop state; if not, In_init state. *)
1243 (* Errors will be reported as exceptions, including LSP ServerErrorStart. *)
1246 let start_time = Unix.time () in
1247 let root = match get_root () with
1248 | None
-> failwith
"we should have root after an initialize request"
1252 let server_conn = connect_client root in
1253 let got_hello = connect_attempt_hello server_conn in
1255 if got_hello then begin
1256 connect_after_hello server_conn ImmQueue.empty
;
1260 uris_with_diagnostics
= SSet.empty
;
1261 ready_dialog_cancel
= None
;
1264 let log_file = Sys_utils.readlink_no_fail
(ServerFiles.log_link
root) in
1265 let clear_cancel_flag state
= match state
with
1266 | In_init ienv
-> In_init
{ienv
with In_init_env.busy_dialog_cancel
= None
}
1268 let handle_result ~state ~
result:_
= clear_cancel_flag state
in
1269 let handle_error ~state ~
code:_ ~
message:_ ~
data:_
= clear_cancel_flag state
in
1270 let req = print_showMessageRequest
MessageType.InfoMessage
1271 "Waiting for Hack server to be ready..." [] in
1272 let cancel = request stdout
handle_result handle_error "window/showMessageRequest" req in
1273 if supports_progress () then begin
1274 let progress = "hh_server initializing" in
1275 print_progress
progress_id_initialize (Some
progress) |> notify stdout
"window/progress"
1281 busy_dialog_cancel
= Some
cancel;
1282 file_edits
= ImmQueue.empty
;
1283 tail_env = Tail.create_env
log_file;
1289 let do_initialize ()
1290 : (Initialize.result * state
) =
1291 let open Initialize
in
1292 let local_config = ServerLocalConfig.load ~silent
:true in
1293 let new_state = connect () in
1295 server_capabilities
= {
1296 textDocumentSync
= {
1297 want_openClose
= true;
1298 want_change
= IncrementalSync
;
1299 want_willSave
= false;
1300 want_willSaveWaitUntil
= false;
1301 want_didSave
= Some
{ includeText
= false }
1303 hoverProvider
= true;
1304 completionProvider
= Some
{
1305 resolveProvider
= false;
1306 completion_triggerCharacters
= ["$"; ">"; "\\"; ":"];
1308 signatureHelpProvider
= None
;
1309 definitionProvider
= true;
1310 referencesProvider
= true;
1311 documentHighlightProvider
= true;
1312 documentSymbolProvider
= true;
1313 workspaceSymbolProvider
= true;
1314 codeActionProvider
= false;
1315 codeLensProvider
= None
;
1316 documentFormattingProvider
= true;
1317 documentRangeFormattingProvider
= true;
1318 documentOnTypeFormattingProvider
=
1319 Option.some_if
local_config.ServerLocalConfig.use_hackfmt
1321 firstTriggerCharacter
= ";";
1322 moreTriggerCharacter
= ["}"];
1324 renameProvider
= false;
1325 documentLinkProvider
= None
;
1326 executeCommandProvider
= None
;
1327 typeCoverageProvider
= true;
1334 let regain_lost_server_if_necessary (state
: state
) (event
: event
) : state
=
1335 (* It's only necessary to regain a lost server if (1) we need it to handle *)
1336 (* a client message, and (2) we lost it in the first place. *)
1337 match event
, state
with
1338 | Client_message _
, Lost_server
->
1339 let (_result
, new_state) = do_initialize () in
1345 let dismiss_ready_dialog_if_necessary (state
: state
) (event
: event
) : state
=
1346 (* We'll auto-dismiss the ready dialog if it was up, in response to user *)
1347 (* actions like typing or hover, and in response to a lost server. *)
1348 let open Jsonrpc_queue
in
1350 | Main_loop
({ready_dialog_cancel
= Some
cancel; _
} as menv) -> begin
1352 | Client_message
{kind
= Jsonrpc_queue.Response
; _
} ->
1355 | Server_message
ServerCommandTypes.NEW_CLIENT_CONNECTED
->
1356 cancel (); Main_loop
{menv with ready_dialog_cancel
= None
}
1363 let handle_idle_if_necessary (state
: state
) (event
: event
) : state
=
1364 match state
, event
with
1365 | Main_loop
menv, Tick
-> Main_loop
{ menv with needs_idle
= true; }
1369 (************************************************************************)
1370 (** Message handling **)
1371 (************************************************************************)
1373 (* handle_event: Process and respond to a message, and update the LSP state
1374 machine accordingly. In case the message was a request, it returns the
1375 json it responded with, so the caller can log it. *)
1379 ~
(client
: Jsonrpc_queue.t
)
1381 : Hh_json.json
option =
1382 let open Jsonrpc_queue
in
1383 match !state
, event
with
1385 | _
, Client_message c
when c
.kind
= Jsonrpc_queue.Response
->
1386 state
:= do_response !state c
.id c
.result c
.error
;
1389 (* exit notification *)
1390 | _
, Client_message c
when c
.method_
= "exit" ->
1391 if !state
= Post_shutdown
then exit_ok () else exit_fail ()
1393 (* initialize request*)
1394 | Pre_init
, Client_message c
when c
.method_
= "initialize" ->
1395 initialize_params := Some
(parse_initialize c
.params);
1396 let (result, new_state) = do_initialize () in
1397 let response = print_initialize
result |> respond stdout c
in
1401 (* any request/notification if we haven't yet initialized *)
1402 | Pre_init
, Client_message _c
->
1403 raise
(Error.ServerNotInitialized
"Server not yet initialized")
1405 (* any request/notification if we're not yet ready *)
1406 | In_init
ienv, Client_message c
->
1407 let open In_init_env
in
1408 begin match c
.method_
with
1409 | "textDocument/didOpen"
1410 | "textDocument/didChange"
1411 | "textDocument/didClose" ->
1412 (* These three crucial-for-correctness notifications will be buffered *)
1413 (* up so we'll be able to handle them when we're ready. *)
1414 state
:= In_init
{ ienv with file_edits
= ImmQueue.push
ienv.file_edits c
}
1416 state
:= Post_shutdown
1418 raise
(Error.RequestCancelled
"Server busy")
1419 (* We deny all other requests. Operation_cancelled is the only *)
1420 (* error-response that won't produce logs/warnings on most clients. *)
1424 (* idle tick while waiting for server to complete initialization *)
1425 | In_init
ienv, Tick
->
1426 report_progress ienv;
1429 (* server completes initialization *)
1430 | In_init
ienv, Server_hello
->
1431 connect_after_hello ienv.In_init_env.conn
ienv.In_init_env.file_edits
;
1432 state
:= report_progress_end ienv;
1435 (* any "hello" from the server when we weren't expecting it. This is so *)
1436 (* egregious that we can't trust anything more from the server. *)
1437 | _
, Server_hello
->
1438 let message = "Unexpected hello" in
1440 raise
(Server_fatal_connection_exception
{ Marshal_tools.message; stack; })
1442 (* Tick when we're connected to the server and have empty queue *)
1443 | Main_loop
menv, Tick
when menv.needs_idle
->
1444 (* If we're connected to a server and have no more messages in the queue, *)
1445 (* then we must let the server know we're idle, so it will be free to *)
1446 (* handle command-line requests. *)
1447 state
:= Main_loop
{ menv with needs_idle
= false; };
1448 rpc menv.conn
ServerCommandTypes.IDE_IDLE
;
1451 (* textDocument/hover request *)
1452 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/hover" ->
1453 cancel_if_stale client c
short_timeout;
1454 parse_hover c
.params |> do_hover menv.conn
|> print_hover
|> respond stdout c
1456 (* textDocument/definition request *)
1457 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/definition" ->
1458 cancel_if_stale client c
short_timeout;
1459 parse_definition c
.params |> do_definition menv.conn
|> print_definition
|> respond stdout c
1461 (* textDocument/completion request *)
1462 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/completion" ->
1464 if env
.use_ffp_autocomplete
then do_completion_ffp else do_completion_legacy in
1465 cancel_if_stale client c
short_timeout;
1466 parse_completion c
.params |> do_completion menv.conn
|> print_completion
|> respond stdout c
1468 (* workspace/symbol request *)
1469 | Main_loop
menv, Client_message c
when c
.method_
= "workspace/symbol" ->
1470 parse_workspaceSymbol c
.params |> do_workspaceSymbol menv.conn
1471 |> print_workspaceSymbol
|> respond stdout c
1473 (* textDocument/documentSymbol request *)
1474 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/documentSymbol" ->
1475 parse_documentSymbol c
.params |> do_documentSymbol menv.conn
1476 |> print_documentSymbol
|> respond stdout c
1478 (* textDocument/references request *)
1479 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/references" ->
1480 cancel_if_stale client c
long_timeout;
1481 parse_findReferences c
.params |> do_findReferences menv.conn
1482 |> print_findReferences
|> respond stdout c
1484 (* textDocument/documentHighlight *)
1485 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/documentHighlight" ->
1486 cancel_if_stale client c
short_timeout;
1487 parse_documentHighlights c
.params |> do_documentHighlights menv.conn
1488 |> print_documentHighlights
|> respond stdout c
1490 (* textDocument/typeCoverage *)
1491 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/typeCoverage" ->
1492 parse_typeCoverage c
.params |> do_typeCoverage menv.conn
1493 |> print_typeCoverage
|> respond stdout c
1495 (* textDocument/formatting *)
1496 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/formatting" ->
1497 parse_documentFormatting c
.params |> do_documentFormatting menv.conn
1498 |> print_documentFormatting
|> respond stdout c
1500 (* textDocument/formatting *)
1501 | Main_loop
menv, Client_message c
1502 when c
.method_
= "textDocument/rangeFormatting" ->
1503 parse_documentRangeFormatting c
.params |> do_documentRangeFormatting menv.conn
1504 |> print_documentRangeFormatting
|> respond stdout c
1506 (* textDocument/onTypeFormatting *)
1507 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/onTypeFormatting" ->
1508 cancel_if_stale client c
short_timeout;
1509 parse_documentOnTypeFormatting c
.params |> do_documentOnTypeFormatting menv.conn
1510 |> print_documentOnTypeFormatting
|> respond stdout c
1512 (* textDocument/didOpen notification *)
1513 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/didOpen" ->
1514 parse_didOpen c
.params |> do_didOpen menv.conn
;
1517 (* textDocument/didClose notification *)
1518 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/didClose" ->
1519 parse_didClose c
.params |> do_didClose menv.conn
;
1522 (* textDocument/didChange notification *)
1523 | Main_loop
menv, Client_message c
when c
.method_
= "textDocument/didChange" ->
1524 parse_didChange c
.params |> do_didChange menv.conn
;
1527 (* textDocument/didSave notification *)
1528 | Main_loop _menv
, Client_message c
when c
.method_
= "textDocument/didSave" ->
1531 (* shutdown request *)
1532 | Main_loop
menv, Client_message c
when c
.method_
= "shutdown" ->
1533 let response = do_shutdown menv.conn
|> print_shutdown
|> respond stdout c
in
1534 state
:= Post_shutdown
;
1537 (* server busy status *)
1538 | _
, Server_message
ServerCommandTypes.BUSY_STATUS status
->
1539 do_server_busy status
;
1542 (* textDocument/publishDiagnostics notification *)
1543 | Main_loop
menv, Server_message
ServerCommandTypes.DIAGNOSTIC
(_
, errors
) ->
1544 let uris_with_diagnostics = do_diagnostics menv.uris_with_diagnostics errors
in
1545 state
:= Main_loop
{ menv with uris_with_diagnostics; };
1548 (* any server diagnostics that come after we've shut down *)
1549 | _
, Server_message
ServerCommandTypes.DIAGNOSTIC _
->
1552 (* catch-all for client reqs/notifications we haven't yet implemented *)
1553 | Main_loop _menv
, Client_message c
->
1554 let message = Printf.sprintf
"not implemented: %s" c
.method_
in
1555 raise
(Error.MethodNotFound
message)
1557 (* catch-all for requests/notifications after shutdown request *)
1558 | Post_shutdown
, Client_message _c
->
1559 raise
(Error.InvalidRequest
"already received shutdown request")
1561 (* server shut-down request *)
1562 | Main_loop
menv, Server_message
ServerCommandTypes.NEW_CLIENT_CONNECTED
->
1563 do_diagnostics_flush menv.uris_with_diagnostics;
1564 state
:= dismiss_ready_dialog_if_necessary !state event
;
1565 state
:= Lost_server
;
1568 (* server shut-down request, unexpected *)
1569 | _
, Server_message
ServerCommandTypes.NEW_CLIENT_CONNECTED
->
1570 let open Marshal_tools
in
1571 let message = "unexpected close of absent server" in
1573 raise
(Server_fatal_connection_exception
{ message; stack; })
1575 (* server fatal shutdown *)
1576 | _
, Server_message
ServerCommandTypes.FATAL_EXCEPTION _
->
1579 (* idle tick. No-op. *)
1583 (* client message when we've lost the server *)
1584 | Lost_server
, Client_message _c
->
1585 (* Our caller should have already transitioned away from this state if *)
1586 (* necessary before calling us, via regain_lost_server_if_necessary. *)
1587 (* If we get here, it's entirely unexpected! don't know how to recover... *)
1588 let open Marshal_tools
in
1589 let message = "unexpected client message for lost server" in
1591 raise
(Server_fatal_connection_exception
{ message; stack; })
1593 (* main: this is the main loop for processing incoming Lsp client requests,
1594 and incoming server notifications. Never returns. *)
1595 let main (env
: env
) : 'a
=
1596 let open Marshal_tools
in
1597 Printexc.record_backtrace
true;
1598 HackEventLogger.client_set_from env
.from
;
1599 let client = Jsonrpc_queue.make
() in
1600 let state = ref Pre_init
in
1602 let ref_event = ref None
in
1603 let start_handle_t = Unix.gettimeofday
() in
1604 (* TODO: we should log how much of the "handling" time was spent *)
1605 (* idle just waiting for an RPC response from hh_server. *)
1607 let event = get_next_event !state client in
1608 ref_event := Some
event;
1609 state := handle_idle_if_necessary !state event;
1610 state := regain_lost_server_if_necessary !state event;
1611 state := dismiss_ready_dialog_if_necessary !state event;
1612 let response = handle_event ~env ~
state ~
client ~
event in
1614 | Client_message c
-> begin
1615 let open Jsonrpc_queue
in
1616 let response_for_logging = match response with
1618 | Some json
-> json
|> Hh_json.json_truncate
|> Hh_json.json_to_string
1620 HackEventLogger.client_lsp_method_handled
1622 ~method_
:(if c
.kind
= Response
then get_outstanding_method_name c
.id else c
.method_
)
1623 ~kind
:(kind_to_string c
.kind
)
1624 ~start_queue_t
:c
.timestamp
1626 ~json
:c
.message_json_for_logging
1627 ~json_response
:response_for_logging;
1631 | Server_fatal_connection_exception edata
->
1632 let stack = edata
.stack ^
"---\n" ^
(Printexc.get_backtrace
()) in
1633 hack_log_error !ref_event edata
.message stack "from_server" start_handle_t;
1634 client_log Lsp.MessageType.ErrorMessage
(edata
.message ^
", from_server\n" ^
stack);
1636 | Client_fatal_connection_exception edata
->
1637 let stack = edata
.stack ^
"---\n" ^
(Printexc.get_backtrace
()) in
1638 hack_log_error !ref_event edata
.message stack "from_client" start_handle_t;
1639 client_log Lsp.MessageType.ErrorMessage
(edata
.message ^
", from_client\n" ^
stack);
1641 | Client_recoverable_connection_exception edata
->
1642 let stack = edata
.stack ^
"---\n" ^
(Printexc.get_backtrace
()) in
1643 hack_log_error !ref_event edata
.message stack "from_client" start_handle_t;
1644 client_log Lsp.MessageType.ErrorMessage
(edata
.message ^
", from_client\n" ^
stack);
1646 let message = Printexc.to_string e
in
1647 let stack = Printexc.get_backtrace
() in
1648 respond_to_error !ref_event e
stack;
1649 hack_log_error !ref_event message stack "from_lsp" start_handle_t;
1651 failwith
"unreachable"