Move client/clientMessageQueue to utils/jsonrpc_queue
[hiphop-php.git] / hphp / hack / src / client / clientLsp.ml
blob22ce12b9f2efaaf506648dceedcb8bc0d6a2ae4f
1 (**
2 * Copyright (c) 2015, Facebook, Inc.
3 * All rights reserved.
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.
9 *)
11 open Core
12 open Lsp
13 open Lsp_fmt
15 (* All hack-specific code relating to LSP goes in here. *)
17 (* The environment for hh_client with LSP *)
18 type env = {
19 from: string; (* The source where the client was spawned from, i.e. nuclide, vim, emacs, etc. *)
20 use_ffp_autocomplete: bool; (* Flag to turn on the (experimental) FFP based autocomplete *)
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
49 File_url.parse uri
50 else
51 raise (Error.InvalidParams (Printf.sprintf "Not a valid file url '%s'" uri))
52 else
53 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)
61 : string =
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 =
66 { Ide_api_types.
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 =
94 { Lsp.
95 start = { Lsp.
96 line = range.Ide_api_types.st.Ide_api_types.line - 1;
97 character = range.Ide_api_types.st.Ide_api_types.column - 1;
99 end_ = { Lsp.
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)
115 : Lsp.Location.t =
116 let open SymbolDefinition in
117 hack_pos_to_lsp_location symbol.pos ~default_path
119 let hack_errors_to_lsp_diagnostic
120 (filename: string)
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;
140 }) in
141 { Lsp.PublishDiagnostics.
142 range;
143 severity = Some PublishDiagnostics.Error;
144 code = Some (Errors.get_code error);
145 source = Some "Hack";
146 message;
147 relatedLocations;
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 (************************************************************************)
162 type server_conn = {
163 ic: Timeout.in_channel;
164 oc: out_channel;
166 (* Pending messages sent from the server. They need to be relayed to the
167 client. *)
168 pending_messages: ServerCommandTypes.push Queue.t;
171 module Main_env = struct
172 type t = {
173 conn: server_conn;
174 needs_idle: bool;
175 uris_with_diagnostics: SSet.t;
176 ready_dialog_cancel: (unit -> unit) option; (* "hack server is now ready" dialog *)
179 open Main_env
181 module In_init_env = struct
182 type t = {
183 conn: server_conn;
184 start_time: float;
185 busy_dialog_cancel: (unit -> unit) option; (* "hack server is busy" dialog *)
186 file_edits: Jsonrpc_queue.jsonrpc_message ImmQueue.t;
187 tail_env: Tail.env;
191 type state =
192 (* Pre_init: we haven't yet received the initialize request. *)
193 | Pre_init
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... *)
203 | Lost_server
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. *)
208 | Post_shutdown
210 let initialize_params: Initialize.params option ref = ref None
212 type event =
213 | Server_hello
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
246 type t = {
247 method_: string;
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
259 match event with
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_
266 | Tick -> "Tick"
269 let state_to_string (state: state) : string =
270 match state with
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
281 | None -> None
282 | Some params ->
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)
302 let rpc
303 (server_conn: server_conn)
304 (command: 'a ServerCommandTypes.t)
305 : 'a =
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);
312 with
313 | ServerCommand.Remote_exception remote_e_data ->
314 raise (Server_fatal_connection_exception remote_e_data)
315 | e ->
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. *)
324 let respond
325 (outchan: out_channel)
326 (c: Jsonrpc_queue.jsonrpc_message)
327 (json: Hh_json.json)
328 : Hh_json.json option =
329 let open Jsonrpc_queue in
330 let open Hh_json 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;
341 Some response
343 (* notify: produces a Notify message *)
344 let notify (outchan: out_channel) (method_: string) (json: Hh_json.json)
345 : unit =
346 let open Hh_json in
347 let message = JSON_Object [
348 "jsonrpc", JSON_String "2.0";
349 "method", JSON_String method_;
350 "params", json;
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 *)
356 let request
357 (outchan: out_channel)
358 (on_result: on_result_callback)
359 (on_error: on_error_callback)
360 (method_: string)
361 (json: Hh_json.json)
362 : unit -> unit =
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;
368 let open Hh_json in
369 let message = JSON_Object [
370 "jsonrpc", string_ "2.0";
371 "id", int_ request_id;
372 "method", string_ method_;
373 "params", json;
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
388 cancel
390 let get_outstanding_request (id: Hh_json.json option) =
391 match id with
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
398 | _ -> None
400 let get_outstanding_method_name (id: Hh_json.json option) : string =
401 let open Callback in
402 match (get_outstanding_request id) with
403 | Some (_, callback) -> callback.method_
404 | None -> ""
406 let do_response
407 (state: state)
408 (id: Hh_json.json option)
409 (result: Hh_json.json option)
410 (error: Hh_json.json option)
411 : state =
412 let open Callback in
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
423 else
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"
430 let hack_log_error
431 (event: event option)
432 (message: string)
433 (stack: string)
434 (source: string)
435 (start_handle_t: float)
436 : unit =
437 let root = get_root () in
438 match event with
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
443 message stack source
444 | _ ->
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
468 else `From_client
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
479 else `From_client
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
488 | Response _ ->
489 failwith "unexpected response without request"
490 | Push m -> Server_message m
491 | Hello -> Server_hello
492 with e ->
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)
518 match state with
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
523 | `No_source -> Tick
525 | _ -> begin
526 match get_client_message_source client with
527 | `From_client -> from_client client
528 | `No_source -> Tick
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
539 let cancel_if_stale
540 (client: Jsonrpc_queue.t)
541 (message: Jsonrpc_queue.jsonrpc_message)
542 (timeout: float)
543 : unit =
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 =
553 match event with
554 | Some (Client_message c)
555 when c.Jsonrpc_queue.kind = Jsonrpc_queue.Request ->
556 print_error e stack |> respond stdout c |> ignore
557 | _ -> ()
560 (************************************************************************)
561 (** Protocol **)
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 =
570 let open DidOpen in
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
575 rpc conn command;
578 let do_didClose (conn: server_conn) (params: DidClose.params) : unit =
579 let open DidClose in
580 let open TextDocumentIdentifier in
581 let filename = lsp_uri_to_path params.textDocument.uri in
582 let command = ServerCommandTypes.CLOSE_FILE filename in
583 rpc conn command;
586 let do_didChange
587 (conn: server_conn)
588 (params: DidChange.params)
589 : unit =
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 =
594 { Ide_api_types.
595 range = Option.map lsp.range lsp_range_to_ide;
596 text = lsp.text;
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
602 rpc conn command;
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... *)
615 | None
616 | Some ("_", _)
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 =
638 match result with
639 | (_, None) -> false
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)
646 else
647 results
649 let rec hack_to_lsp = function
650 | [] -> []
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 *)
679 sortText = None;
680 filterText = None;
681 insertText = Some insertText;
682 insertTextFormat = insertTextFormat;
683 textEdits = [];
684 command = None;
685 data = None;
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 *)
697 | Interface_kind
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
718 | Some details ->
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)
730 | _ ->
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
757 (conn: server_conn)
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
786 | _ -> None
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) =
793 { SymbolInformation.
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
803 (conn: server_conn)
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 =
833 { SymbolInformation.
834 name = definition.name;
835 kind = hack_to_lsp_kind definition.kind;
836 location = hack_symbol_definition_to_lsp_location definition ~default_path:filename;
837 containerName;
840 let rec hack_symbol_tree_to_lsp ~accu ~container_name = function
841 (* Flattens the recursive list of symbols *)
842 | [] -> List.rev accu
843 | def :: defs ->
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
852 (conn: server_conn)
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 *)
864 match results with
865 | None -> []
866 | Some (_name, positions) ->
867 List.map positions ~f:(hack_pos_to_lsp_location ~default_path:filename)
870 let do_documentHighlights
871 (conn: server_conn)
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;
883 kind = None;
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
905 match level with
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
919 match level with
920 | Ide_api_types.Checked -> None
921 | Ide_api_types.Unchecked -> Some
922 { range;
923 message = "Un-type checked code. Consider adding type annotations.";
925 | Ide_api_types.Partial -> Some
926 { range;
927 message = "Partially type checked code. Consider adding type annotations.";
931 coveredPercent;
932 uncoveredRanges = List.filter_map results ~f:hack_coverage_to_lsp;
936 let do_formatting_common
937 (conn: server_conn)
938 (args: ServerFormatTypes.ide_action)
939 : TextEdit.t list =
940 let open ServerFormatTypes in
941 let command = ServerCommandTypes.IDE_FORMAT args in
942 let response: ServerFormatTypes.ide_result = rpc conn command in
943 match response with
944 | Result.Error message ->
945 raise (Error.InternalError message)
946 | Result.Ok r ->
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
953 (conn: server_conn)
954 (params: DocumentRangeFormatting.params)
955 : DocumentRangeFormatting.result =
956 let open DocumentRangeFormatting in
957 let open TextDocumentIdentifier in
958 let action = ServerFormatTypes.Range
959 { Ide_api_types.
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
968 (conn: server_conn)
969 (params: DocumentOnTypeFormatting.params)
970 : DocumentOnTypeFormatting.result =
971 let open DocumentOnTypeFormatting in
972 let open TextDocumentIdentifier in
973 let action = ServerFormatTypes.Position
974 { Ide_api_types.
975 filename = lsp_uri_to_path params.textDocument.uri;
976 position = lsp_position_to_ide params.position;
977 } in
978 do_formatting_common conn action
981 let do_documentFormatting
982 (conn: server_conn)
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. *)
1009 let do_diagnostics
1010 (uris_with_diagnostics: SSet.t)
1011 (file_reports: Pos.absolute Errors.error_ list SMap.t)
1012 : SSet.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)
1049 : unit =
1050 let per_uri uri =
1051 { Lsp.PublishDiagnostics.uri = uri;
1052 diagnostics = [];
1054 |> print_diagnostics
1055 |> notify stdout "textDocument/publishDiagnostics"
1057 SSet.iter per_uri diagnostic_uris
1060 let report_progress
1061 (ienv: In_init_env.t)
1062 : unit =
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
1080 Printf.sprintf
1081 "hh_server initializing (load-state not found - will take a while): %s [%i seconds]"
1082 tail_msg delay_in_secs
1083 else
1084 Printf.sprintf
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)
1094 : state =
1095 let open In_init_env in
1096 let menv =
1097 { Main_env.
1098 conn = ienv.In_init_env.conn;
1099 needs_idle = true;
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}
1116 | _ -> state
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";
1125 Main_loop menv
1126 end else
1127 Main_loop menv
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)
1135 : unit =
1136 let open Marshal_tools in
1137 begin try
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;
1154 with e ->
1155 let message = Printexc.to_string e in
1156 let stack = Printexc.get_backtrace () in
1157 raise (Server_fatal_connection_exception { message; stack; })
1158 end;
1160 rpc server_conn (ServerCommandTypes.SUBSCRIBE_DIAGNOSTIC 0)
1163 let connect_client
1164 (root: Path.t)
1165 : server_conn =
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. *)
1174 let env_connect =
1175 { ClientConnect.
1176 root;
1177 autostart = true;
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 *)
1181 no_load = false;
1182 profile_log = false; (* irrelevant *)
1183 ai_mode = None;
1184 progress_callback = ClientConnect.null_progress_reporter; (* we're fast! *)
1185 do_post_handoff_handshake = false;
1186 } in
1188 let (ic, oc) = ClientConnect.connect env_connect in
1189 let pending_messages = Queue.create () in
1190 { ic; oc; pending_messages; }
1191 with
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)
1218 : bool =
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;
1228 true
1229 with
1230 | Exit_status.Exit_with Exit_status.Out_of_retries ->
1231 false
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. *)
1244 let connect ()
1245 : state =
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"
1249 | Some root -> root
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;
1257 Main_loop {
1258 conn = server_conn;
1259 needs_idle = true;
1260 uris_with_diagnostics = SSet.empty;
1261 ready_dialog_cancel = None;
1263 end else begin
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}
1267 | _ -> state in
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"
1276 end;
1277 let ienv =
1278 { In_init_env.
1279 conn = server_conn;
1280 start_time;
1281 busy_dialog_cancel = Some cancel;
1282 file_edits = ImmQueue.empty;
1283 tail_env = Tail.create_env log_file;
1284 } in
1285 In_init ienv
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
1294 let result = {
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;
1331 (result, new_state)
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
1340 new_state
1341 | _ ->
1342 state
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
1349 match state with
1350 | Main_loop ({ready_dialog_cancel = Some cancel; _} as menv) -> begin
1351 match event with
1352 | Client_message {kind = Jsonrpc_queue.Response; _} ->
1353 state
1354 | Client_message _
1355 | Server_message ServerCommandTypes.NEW_CLIENT_CONNECTED ->
1356 cancel (); Main_loop {menv with ready_dialog_cancel = None}
1357 | _ ->
1358 state
1360 | _ -> state
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; }
1366 | _ -> state
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. *)
1376 let handle_event
1377 ~(env: env)
1378 ~(state: state ref)
1379 ~(client: Jsonrpc_queue.t)
1380 ~(event: event)
1381 : Hh_json.json option =
1382 let open Jsonrpc_queue in
1383 match !state, event with
1384 (* response *)
1385 | _, Client_message c when c.kind = Jsonrpc_queue.Response ->
1386 state := do_response !state c.id c.result c.error;
1387 None
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
1398 state := new_state;
1399 response
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 }
1415 | "shutdown" ->
1416 state := Post_shutdown
1417 | _ ->
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. *)
1421 end;
1422 None
1424 (* idle tick while waiting for server to complete initialization *)
1425 | In_init ienv, Tick ->
1426 report_progress ienv;
1427 None
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;
1433 None
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
1439 let stack = "" 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;
1449 None
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" ->
1463 let do_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;
1515 None
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;
1520 None
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;
1525 None
1527 (* textDocument/didSave notification *)
1528 | Main_loop _menv, Client_message c when c.method_ = "textDocument/didSave" ->
1529 None
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;
1535 response
1537 (* server busy status *)
1538 | _, Server_message ServerCommandTypes.BUSY_STATUS status ->
1539 do_server_busy status;
1540 None
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; };
1546 None
1548 (* any server diagnostics that come after we've shut down *)
1549 | _, Server_message ServerCommandTypes.DIAGNOSTIC _ ->
1550 None
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;
1566 None
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
1572 let stack = "" in
1573 raise (Server_fatal_connection_exception { message; stack; })
1575 (* server fatal shutdown *)
1576 | _, Server_message ServerCommandTypes.FATAL_EXCEPTION _ ->
1577 exit_fail_delay ()
1579 (* idle tick. No-op. *)
1580 | _, Tick ->
1581 None
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
1590 let stack = "" 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
1601 while true do
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
1613 match event with
1614 | Client_message c -> begin
1615 let open Jsonrpc_queue in
1616 let response_for_logging = match response with
1617 | None -> ""
1618 | Some json -> json |> Hh_json.json_truncate |> Hh_json.json_to_string
1620 HackEventLogger.client_lsp_method_handled
1621 ~root:(get_root ())
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
1625 ~start_handle_t
1626 ~json:c.message_json_for_logging
1627 ~json_response:response_for_logging;
1629 | _ -> ()
1630 with
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);
1635 exit_fail_delay ()
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);
1640 exit_fail ()
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);
1645 | e ->
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;
1650 done;
1651 failwith "unreachable"