-
Notifications
You must be signed in to change notification settings - Fork 3k
/
clientLsp.ml
1852 lines (1663 loc) · 76.1 KB
/
clientLsp.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(**
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the "hack" directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*)
open Hh_core
open Lsp
open Lsp_fmt
(* All hack-specific code relating to LSP goes in here. *)
(* The environment for hh_client with LSP *)
type env = {
from: string; (* The source where the client was spawned from, i.e. nuclide, vim, emacs, etc. *)
use_ffp_autocomplete: bool; (* Flag to turn on the (experimental) FFP based autocomplete *)
}
(************************************************************************)
(** Protocol orchestration & helpers **)
(************************************************************************)
type server_conn = {
ic: Timeout.in_channel;
oc: out_channel;
(* Pending messages sent from the server. They need to be relayed to the
client. *)
pending_messages: ServerCommandTypes.push Queue.t;
}
module Main_env = struct
type t = {
conn: server_conn;
needs_idle: bool;
uris_with_diagnostics: SSet.t;
uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
dialog: ShowMessageRequest.t; (* "hack server is now ready" *)
progress: Progress.t; (* "typechecking..." *)
actionRequired: ActionRequired.t; (* "save any file to trigger a global recheck" *)
}
end
module In_init_env = struct
type t = {
conn: server_conn;
first_start_time: float; (* our first attempt to connect *)
most_recent_start_time: float; (* for subsequent retries *)
file_edits: Hh_json.json ImmQueue.t;
uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
tail_env: Tail.env option;
has_reported_progress: bool;
dialog: ShowMessageRequest.t; (* "hack server is busy" *)
progress: Progress.t; (* "hh_server is initializing [naming]" *)
}
end
module Lost_env = struct
type t = {
p: params;
uris_with_unsaved_changes: SSet.t; (* see comment in get_uris_with_unsaved_changes *)
lock_file: string;
dialog: ShowMessageRequest.t; (* "hh_server stopped" *)
actionRequired: ActionRequired.t; (* "hh_server stopped" *)
progress: Progress.t; (* "hh_server monitor is waiting for a rebase to settle" *)
}
and how_to_explain_loss_to_user =
| Action_required of string (* explain via dialog and actionRequired *)
| Wait_required of string (* explain via progress *)
and params = {
explanation: how_to_explain_loss_to_user;
start_on_click: bool; (* if user clicks Restart, do we ClientStart before reconnecting? *)
trigger_on_lsp: bool; (* reconnect if we receive any LSP request/notification *)
trigger_on_lock_file: bool; (* reconnect if lockfile is created *)
}
end
type state =
(* Pre_init: we haven't yet received the initialize request. *)
| Pre_init
(* In_init: we did respond to the initialize request, and now we're *)
(* waiting for a "Hello" from the server. When that comes we'll *)
(* request a permanent connection from the server, and process the *)
(* file_changes backlog, and switch to Main_loop. *)
| In_init of In_init_env.t
(* Main_loop: we have a working connection to both server and client. *)
| Main_loop of Main_env.t
(* Lost_server: someone stole the persistent connection from us. *)
(* We might choose to grab it back if prompted... *)
| Lost_server of Lost_env.t
(* Post_shutdown: we received a shutdown request from the client, and *)
(* therefore shut down our connection to the server. We can't handle *)
(* any more requests from the client and will close as soon as it *)
(* notifies us that we can exit. *)
| Post_shutdown
let initialize_params: Hh_json.json option ref = ref None
let hhconfig_version: string ref = ref "[NotYetInitialized]"
let can_autostart_after_mismatch: bool ref = ref true
module Jsonrpc = Jsonrpc.Make (struct type t = state end)
module Lsp_helpers = Lsp_helpers.Make (Jsonrpc) (struct let get () = !initialize_params end)
let to_stdout (json: Hh_json.json) : unit =
let s = (Hh_json.json_to_string json) ^ "\r\n\r\n" in
Http_lite.write_message stdout s
type event =
| Server_hello
| Server_message of ServerCommandTypes.push
| Client_message of Jsonrpc.message
| Tick (* once per second, on idle *)
(* Here are some exit points. *)
let exit_ok () = exit 0
let exit_fail () = exit 1
(* The following connection exceptions inform the main LSP event loop how to *)
(* respond to an exception: was the exception a connection-related exception *)
(* (one of these) or did it arise during other logic (not one of these)? Can *)
(* we report the exception to the LSP client? Can we continue handling *)
(* further LSP messages or must we quit? If we quit, can we do so immediately *)
(* or must we delay? -- Separately, they also help us marshal callstacks *)
(* across daemon- and process-boundaries. *)
exception Client_fatal_connection_exception of Marshal_tools.remote_exception_data
exception Client_recoverable_connection_exception of Marshal_tools.remote_exception_data
exception Server_fatal_connection_exception of Marshal_tools.remote_exception_data
let event_to_string (event: event) : string =
let open Jsonrpc in
match event with
| Server_hello -> "Server hello"
| Server_message ServerCommandTypes.DIAGNOSTIC _ -> "Server DIAGNOSTIC"
| Server_message ServerCommandTypes.BUSY_STATUS _ -> "Server BUSY_STATUS"
| Server_message ServerCommandTypes.NEW_CLIENT_CONNECTED -> "Server NEW_CLIENT_CONNECTED"
| Server_message ServerCommandTypes.FATAL_EXCEPTION _ -> "Server FATAL_EXCEPTION"
| Client_message c -> Printf.sprintf "Client %s %s" (kind_to_string c.kind) c.method_
| Tick -> "Tick"
let state_to_string (state: state) : string =
match state with
| Pre_init -> "Pre_init"
| In_init _ienv -> "In_init"
| Main_loop _menv -> "Main_loop"
| Lost_server _lenv -> "Lost_server"
| Post_shutdown -> "Post_shutdown"
let get_root () : Path.t option =
let path_opt = Lsp_helpers.get_root () in
if Option.is_none path_opt then
None (* None for us means "haven't yet received initialize so we don't know" *)
else
Some (ClientArgsUtils.get_root path_opt) (* None for ClientArgsUtils means "." *)
let read_hhconfig_version () : string =
match get_root () with
| None ->
"[NoRoot]"
| Some root ->
let file = Filename.concat (Path.to_string root) ".hhconfig" in
try
let contents = Sys_utils.cat file in
let config = Config_file.parse_contents contents in
let version = SMap.get "version" config in
Option.value version ~default:"[NoVersion]"
with e ->
Printf.sprintf "[NoHhconfig:%s]" (Printexc.to_string e)
(* get_uris_with_unsaved_changes is the set of files for which we've *)
(* received didChange but haven't yet received didSave/didOpen. It is purely *)
(* a description of what we've heard of the editor, and is independent of *)
(* whether or not they've yet been synced with hh_server. *)
(* As it happens: in Main_loop state all these files will already have been *)
(* sent to hh_server; in In_init state all these files will have been queued *)
(* up inside file_edits ready to be sent when we receive the hello; in *)
(* Lost_server state they're not even queued up, and if ever we see hh_server *)
(* ready then we'll terminate the LSP server and trust the client to relaunch *)
(* us and resend a load of didOpen/didChange events. *)
let get_uris_with_unsaved_changes (state: state): SSet.t =
match state with
| Main_loop menv -> menv.Main_env.uris_with_unsaved_changes
| In_init ienv -> ienv.In_init_env.uris_with_unsaved_changes
| Lost_server lenv -> lenv.Lost_env.uris_with_unsaved_changes
| _ -> SSet.empty
let rpc
(server_conn: server_conn)
(command: 'a ServerCommandTypes.t)
: 'a =
try
let res, pending_messages =
ServerCommand.rpc_persistent (server_conn.ic, server_conn.oc) command in
List.iter pending_messages
~f:(fun x -> Queue.push x server_conn.pending_messages);
res
with
| ServerCommand.Remote_exception remote_e_data ->
raise (Server_fatal_connection_exception remote_e_data)
| e ->
let message = Printexc.to_string e in
let stack = Printexc.get_backtrace () in
raise (Server_fatal_connection_exception { Marshal_tools.message; stack; })
(* Determine whether to read a message from the client (the editor) or the
server (hh_server), or whether neither is ready within 1s. *)
let get_message_source
(server: server_conn)
(client: Jsonrpc.queue)
: [> `From_server | `From_client | `No_source ] =
(* Take action on server messages in preference to client messages, because
server messages are very easy and quick to service (just send a message to
the client), while client messages require us to launch a potentially
long-running RPC command. *)
let has_server_messages = not (Queue.is_empty server.pending_messages) in
if has_server_messages then `From_server else
if Jsonrpc.has_message client then `From_client else
(* If no immediate messages are available, then wait up to 1 second. *)
let server_read_fd = Unix.descr_of_out_channel server.oc in
let client_read_fd = Jsonrpc.get_read_fd client in
let readable, _, _ = Unix.select [server_read_fd; client_read_fd] [] [] 1.0 in
if readable = [] then `No_source
else if List.mem readable server_read_fd then `From_server
else `From_client
(* A simplified version of get_message_source which only looks at client *)
let get_client_message_source
(client: Jsonrpc.queue)
: [> `From_client | `No_source ] =
if Jsonrpc.has_message client then `From_client else
let client_read_fd = Jsonrpc.get_read_fd client in
let readable, _, _ = Unix.select [client_read_fd] [] [] 1.0 in
if readable = [] then `No_source
else `From_client
(* Read a message unmarshaled from the server's out_channel. *)
let read_message_from_server (server: server_conn) : event =
let open ServerCommandTypes in
try
let fd = Unix.descr_of_out_channel server.oc in
match Marshal_tools.from_fd_with_preamble fd with
| Response _ ->
failwith "unexpected response without request"
| Push m -> Server_message m
| Hello -> Server_hello
with e ->
let message = Printexc.to_string e in
let stack = Printexc.get_backtrace () in
raise (Server_fatal_connection_exception { Marshal_tools.message; stack; })
(* get_next_event: picks up the next available message from either client or
server. The way it's implemented, at the first character of a message
from either client or server, we block until that message is completely
received. Note: if server is None (meaning we haven't yet established
connection with server) then we'll just block waiting for client. *)
let get_next_event (state: state) (client: Jsonrpc.queue) : event =
let from_server (server: server_conn) =
if Queue.is_empty server.pending_messages
then read_message_from_server server
else Server_message (Queue.take server.pending_messages)
in
let from_client (client: Jsonrpc.queue) =
match Jsonrpc.get_message client with
| `Message message -> Client_message message
| `Fatal_exception edata -> raise (Client_fatal_connection_exception edata)
| `Recoverable_exception edata -> raise (Client_recoverable_connection_exception edata)
in
match state with
| Main_loop { Main_env.conn; _ } | In_init { In_init_env.conn; _ } -> begin
match get_message_source conn client with
| `From_client -> from_client client
| `From_server -> from_server conn
| `No_source -> Tick
end
| _ -> begin
match get_client_message_source client with
| `From_client -> from_client client
| `No_source -> Tick
end
(* respond_to_error: if we threw an exception during the handling of a request,
report the exception to the client as the response to their request. *)
let respond_to_error (event: event option) (e: exn) (stack: string): unit =
match event with
| Some (Client_message c)
when c.Jsonrpc.kind = Jsonrpc.Request ->
print_error e stack |> Jsonrpc.respond to_stdout c
| _ ->
let (code, message, _original_data) = get_error_info e in
Lsp_helpers.telemetry_error to_stdout (Printf.sprintf "%s [%i]\n%s" message code stack)
(* dismiss_ui: dismisses all dialogs, progress- and action-required *)
(* indicators and diagnostics in a state. *)
let dismiss_ui (state: state) : state =
match state with
| In_init ienv ->
let open In_init_env in
Option.iter ~f:Tail.close_env ienv.tail_env;
In_init { ienv with
tail_env = None;
dialog = Lsp_helpers.dismiss_showMessageRequest ienv.dialog;
progress = Lsp_helpers.notify_progress to_stdout ienv.progress None;
}
| Main_loop menv ->
let open Main_env in
Main_loop { menv with
uris_with_diagnostics = Lsp_helpers.dismiss_diagnostics to_stdout menv.uris_with_diagnostics;
dialog = Lsp_helpers.dismiss_showMessageRequest menv.dialog;
progress = Lsp_helpers.notify_progress to_stdout menv.progress None;
actionRequired = Lsp_helpers.notify_actionRequired to_stdout menv.actionRequired None;
}
| Lost_server lenv ->
let open Lost_env in
Lost_server { lenv with
dialog = Lsp_helpers.dismiss_showMessageRequest lenv.dialog;
actionRequired = Lsp_helpers.notify_actionRequired to_stdout lenv.actionRequired None;
progress = Lsp_helpers.notify_progress to_stdout lenv.progress None;
}
| Pre_init -> Pre_init
| Post_shutdown -> Post_shutdown
(************************************************************************)
(** Conversions - ad-hoc ones written as needed them, not systematic **)
(************************************************************************)
let lsp_uri_to_path = Lsp_helpers.lsp_uri_to_path
let path_to_lsp_uri = Lsp_helpers.path_to_lsp_uri
let lsp_position_to_ide (position: Lsp.position) : Ide_api_types.position =
{ Ide_api_types.
line = position.line + 1;
column = position.character + 1;
}
let lsp_file_position_to_hack (params: Lsp.TextDocumentPositionParams.t)
: string * int * int =
let open Lsp.TextDocumentPositionParams in
let {Ide_api_types.line; column;} = lsp_position_to_ide params.position in
let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument
in
(filename, line, column)
let hack_pos_to_lsp_range (pos: 'a Pos.pos) : Lsp.range =
let line1, col1, line2, col2 = Pos.destruct_range pos in
{
start = {line = line1 - 1; character = col1 - 1;};
end_ = {line = line2 - 1; character = col2 - 1;};
}
let hack_pos_to_lsp_location (pos: string Pos.pos) ~(default_path: string): Lsp.Location.t =
let open Lsp.Location in
{
uri = path_to_lsp_uri (Pos.filename pos) ~default_path;
range = hack_pos_to_lsp_range pos;
}
let ide_range_to_lsp (range: Ide_api_types.range) : Lsp.range =
{ Lsp.
start = { Lsp.
line = range.Ide_api_types.st.Ide_api_types.line - 1;
character = range.Ide_api_types.st.Ide_api_types.column - 1;
};
end_ = { Lsp.
line = range.Ide_api_types.ed.Ide_api_types.line - 1;
character = range.Ide_api_types.ed.Ide_api_types.column - 1;
};
}
let lsp_range_to_ide (range: Lsp.range) : Ide_api_types.range =
let open Ide_api_types in
{
st = lsp_position_to_ide range.start;
ed = lsp_position_to_ide range.end_;
}
let hack_symbol_definition_to_lsp_location
(symbol: string SymbolDefinition.t)
~(default_path: string)
: Lsp.Location.t =
let open SymbolDefinition in
hack_pos_to_lsp_location symbol.pos ~default_path
let hack_errors_to_lsp_diagnostic
(filename: string)
(errors: Pos.absolute Errors.error_ list)
: PublishDiagnostics.params =
let open Lsp.Location in
let location_message (error: Pos.absolute * string) : (Lsp.Location.t * string) =
let (pos, message) = error in
let {uri; range;} = hack_pos_to_lsp_location pos ~default_path:filename in
({Location.uri; range;}, message)
in
let hack_error_to_lsp_diagnostic (error: Pos.absolute Errors.error_) =
let all_messages = Errors.to_list error |> List.map ~f:location_message in
let (first_message, additional_messages) = match all_messages with
| hd :: tl -> (hd, tl)
| [] -> failwith "Expected at least one error in the error list"
in
let ({range; _}, message) = first_message in
let relatedLocations = additional_messages |> List.map ~f:(fun (location, message) ->
{ PublishDiagnostics.
relatedLocation = location;
relatedMessage = message;
}) in
{ Lsp.PublishDiagnostics.
range;
severity = Some PublishDiagnostics.Error;
code = Some (Errors.get_code error);
source = Some "Hack";
message;
relatedLocations;
}
in
(* The caller is required to give us a non-empty filename. If it is empty, *)
(* the following path_to_lsp_uri will fall back to the default path - which *)
(* is also empty - and throw, logging appropriate telemetry. *)
{ Lsp.PublishDiagnostics.
uri = path_to_lsp_uri filename ~default_path:"";
diagnostics = List.map errors ~f:hack_error_to_lsp_diagnostic;
}
(************************************************************************)
(** Protocol **)
(************************************************************************)
let do_shutdown (state: state) : state =
let state = dismiss_ui state in
begin match state with
| Main_loop menv ->
(* In Main_loop state, we're expected to unsubscribe diagnostics and tell *)
(* server to disconnect so it can revert the state of its unsaved files. *)
let open Main_env in
rpc menv.conn (ServerCommandTypes.UNSUBSCRIBE_DIAGNOSTIC 0);
rpc menv.conn (ServerCommandTypes.DISCONNECT)
| In_init _ienv ->
(* In In_init state, even though we have a 'conn', it's still waiting for *)
(* the server to become responsive, so there's no use sending any rpc *)
(* messages to the server over it. *)
()
| _ ->
(* No other states have a 'conn' to send any disconnect messages over. *)
()
end;
Post_shutdown
let do_rage (state: state) : Rage.result =
let open Rage in
let logItems = match get_root () with
| None -> []
| Some root ->
let monitor_log_link = ServerFiles.monitor_log_link root in
let log_link = ServerFiles.log_link root in
[
{
title = Some log_link;
data = Sys_utils.cat log_link;
};
{
title = Some monitor_log_link;
data = Sys_utils.cat monitor_log_link;
}
]
in
let clientItems = [
{
title = None;
data = "LSP adapter state: " ^ (state_to_string state);
};
]
in
let serverItems = match state with
| Main_loop menv ->
let open Main_env in
let items = rpc menv.conn ServerCommandTypes.RAGE in
let hack_to_lsp item = {
title = item.ServerRageTypes.title;
data = item.ServerRageTypes.data;
} in
List.map items ~f:hack_to_lsp
| _ ->
[]
in
clientItems @ logItems @ serverItems
let do_didOpen (conn: server_conn) (params: DidOpen.params) : unit =
let open DidOpen in
let open TextDocumentItem in
let filename = lsp_uri_to_path params.textDocument.uri in
let text = params.textDocument.text in
let command = ServerCommandTypes.OPEN_FILE (filename, text) in
rpc conn command;
()
let do_didClose (conn: server_conn) (params: DidClose.params) : unit =
let open DidClose in
let open TextDocumentIdentifier in
let filename = lsp_uri_to_path params.textDocument.uri in
let command = ServerCommandTypes.CLOSE_FILE filename in
rpc conn command;
()
let do_didChange
(conn: server_conn)
(params: DidChange.params)
: unit =
let open VersionedTextDocumentIdentifier in
let open Lsp.DidChange in
let lsp_change_to_ide (lsp: DidChange.textDocumentContentChangeEvent)
: Ide_api_types.text_edit =
{ Ide_api_types.
range = Option.map lsp.range lsp_range_to_ide;
text = lsp.text;
}
in
let filename = lsp_uri_to_path params.textDocument.uri in
let changes = List.map params.contentChanges ~f:lsp_change_to_ide in
let command = ServerCommandTypes.EDIT_FILE (filename, changes) in
rpc conn command;
()
let do_hover (conn: server_conn) (params: Hover.params) : Hover.result =
(* TODO: should return MarkedCode, once Nuclide supports it *)
(* TODO: should return doc-comment as well *)
(* TODO: should return signature of what we hovered on, not just type. *)
let (file, line, column) = lsp_file_position_to_hack params in
let command = ServerCommandTypes.INFER_TYPE (ServerUtils.FileName file, line, column) in
let inferred_type = rpc conn command in
match inferred_type with
(* Hack server uses both None and "_" to indicate absence of a result. *)
(* We're also catching the non-result "" just in case... *)
| None
| Some ("_", _)
| Some ("", _) -> { Hover.contents = []; range = None; }
| Some (s, _) -> { Hover.contents = [MarkedString s]; range = None; }
let do_definition (conn: server_conn) (params: Definition.params)
: Definition.result =
let (file, line, column) = lsp_file_position_to_hack params in
let command = ServerCommandTypes.IDENTIFY_FUNCTION (ServerUtils.FileName file, line, column) in
let results = rpc conn command in
(* What's it like when we return multiple definitions? For instance, if you ask *)
(* for the definition of "new C()" then we've now got the definition of the *)
(* class "\C" and also of the constructor "\\C::__construct". I think that *)
(* users would be happier to only have the definition of the constructor, so *)
(* as to jump straight to it without the fuss of clicking to select which one. *)
(* That indeed is what Typescript does -- it only gives the constructor. *)
(* (VSCode displays multiple definitions with a peek view of them all; *)
(* Atom displays them with a small popup showing just file+line of each). *)
(* There's one subtlety. If you declare a base class "B" with a constructor, *)
(* and a derived class "C" without a constructor, and click on "new C()", then *)
(* both Hack and Typescript will take you to the constructor of B. As desired! *)
(* Conclusion: given a class+method, we'll return only the method. *)
let result_is_method (result: IdentifySymbolService.single_result): bool =
match result with
| { SymbolOccurrence.type_ = SymbolOccurrence.Method _; _ }, _ -> true
| _ -> false in
let result_is_class (result: IdentifySymbolService.single_result): bool =
match result with
| { SymbolOccurrence.type_ = SymbolOccurrence.Class; _ }, _ -> true
| _ -> false in
let has_class = List.exists results ~f:result_is_class in
let has_method = List.exists results ~f:result_is_method in
let filtered_results = if has_class && has_method then
List.filter results ~f:result_is_method
else
results
in
let rec hack_to_lsp = function
| [] -> []
| (_occurrence, None) :: l -> hack_to_lsp l
| (_occurrence, Some definition) :: l ->
(hack_symbol_definition_to_lsp_location definition ~default_path:file) :: (hack_to_lsp l)
in
hack_to_lsp filtered_results
let make_ide_completion_response (result:AutocompleteTypes.ide_result) =
let open AutocompleteTypes in
let open Completion in
(* We use snippets to provide parentheses+arguments when autocompleting *)
(* method calls e.g. "$c->|" ==> "$c->foo($arg1)". But we'll only do this *)
(* there's nothing after the caret: no "$c->|(1)" -> "$c->foo($arg1)(1)" *)
let is_caret_followed_by_lparen = result.char_at_pos = '(' in
let rec hack_completion_to_lsp (completion: complete_autocomplete_result)
: Completion.completionItem =
let (insertText, insertTextFormat) = hack_to_insert completion in
{
label = completion.res_name ^ (if completion.res_kind = Namespace_kind then "\\" else "");
kind = hack_to_kind completion;
detail = Some (hack_to_detail completion);
inlineDetail = Some (hack_to_inline_detail completion);
itemType = hack_to_itemType completion;
documentation = None; (* TODO: provide doc-comments *)
sortText = None;
filterText = None;
insertText = Some insertText;
insertTextFormat = insertTextFormat;
textEdits = [];
command = None;
data = None;
}
and hack_to_kind (completion: complete_autocomplete_result)
: Completion.completionItemKind option =
match completion.res_kind with
| Abstract_class_kind
| Class_kind -> Some Completion.Class
| Method_kind -> Some Completion.Method
| Function_kind -> Some Completion.Function
| Variable_kind -> Some Completion.Variable
| Property_kind -> Some Completion.Property
| Class_constant_kind -> Some Completion.Value (* a bit off, but the best we can do *)
| Interface_kind
| Trait_kind -> Some Completion.Interface
| Enum_kind -> Some Completion.Enum
| Namespace_kind -> Some Completion.Module
| Constructor_kind -> Some Completion.Constructor
| Keyword_kind -> Some Completion.Keyword
and hack_to_itemType (completion: complete_autocomplete_result) : string option =
(* TODO: we're using itemType (left column) for function return types, and *)
(* the inlineDetail (right column) for variable/field types. Is that good? *)
Option.map completion.func_details ~f:(fun details -> details.return_ty)
and hack_to_detail (completion: complete_autocomplete_result) : string =
(* TODO: retrieve the actual signature including name+modifiers *)
(* For now we just return the type of the completion. In the case *)
(* of functions, their function-types have parentheses around them *)
(* which we want to strip. In other cases like tuples, no strip. *)
match completion.func_details with
| None -> completion.res_ty
| Some _ -> String_utils.rstrip (String_utils.lstrip completion.res_ty "(") ")"
and hack_to_inline_detail (completion: complete_autocomplete_result) : string =
match completion.func_details with
| None -> hack_to_detail completion
| Some details ->
(* "(type1 $param1, ...)" *)
let f param = Printf.sprintf "%s %s" param.param_ty param.param_name in
let params = String.concat ", " (List.map details.params ~f) in
Printf.sprintf "(%s)" params
and hack_to_insert (completion: complete_autocomplete_result) : (string * insertTextFormat) =
match completion.func_details with
| Some details when Lsp_helpers.supports_snippets () && not is_caret_followed_by_lparen ->
(* "method(${1:arg1}, ...)" but for args we just use param names. *)
let f i param = Printf.sprintf "${%i:%s}" (i + 1) param.param_name in
let params = String.concat ", " (List.mapi details.params ~f) in
(Printf.sprintf "%s(%s)" completion.res_name params, SnippetFormat)
| _ ->
(completion.res_name, PlainText)
in
{
isIncomplete = not result.is_complete;
items = List.map result.completions ~f:hack_completion_to_lsp;
}
let do_completion_ffp (conn: server_conn) (params: Completion.params) : Completion.result =
let open TextDocumentIdentifier in
let pos = lsp_position_to_ide params.TextDocumentPositionParams.position in
let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument.uri in
let command = ServerCommandTypes.IDE_FFP_AUTOCOMPLETE (filename, pos) in
let result = rpc conn command in
make_ide_completion_response result
let do_completion_legacy (conn: server_conn) (params: Completion.params)
: Completion.result =
let open TextDocumentIdentifier in
let pos = lsp_position_to_ide params.TextDocumentPositionParams.position in
let filename = lsp_uri_to_path params.TextDocumentPositionParams.textDocument.uri in
let delimit_on_namespaces = true in
let command = ServerCommandTypes.IDE_AUTOCOMPLETE (filename, pos, delimit_on_namespaces) in
let result = rpc conn command in
make_ide_completion_response result
let do_workspaceSymbol
(conn: server_conn)
(params: WorkspaceSymbol.params)
: WorkspaceSymbol.result =
let open WorkspaceSymbol in
let open SearchUtils in
let query = params.query in
let query_type = "" in
let command = ServerCommandTypes.SEARCH (query, query_type) in
let results = rpc conn command in
let hack_to_lsp_kind = function
| HackSearchService.Class (Some Ast.Cabstract) -> SymbolInformation.Class
| HackSearchService.Class (Some Ast.Cnormal) -> SymbolInformation.Class
| HackSearchService.Class (Some Ast.Cinterface) -> SymbolInformation.Interface
| HackSearchService.Class (Some Ast.Ctrait) -> SymbolInformation.Interface
(* LSP doesn't have traits, so we approximate with interface *)
| HackSearchService.Class (Some Ast.Cenum) -> SymbolInformation.Enum
| HackSearchService.Class (None) -> assert false (* should never happen *)
| HackSearchService.Method _ -> SymbolInformation.Method
| HackSearchService.ClassVar _ -> SymbolInformation.Property
| HackSearchService.Function -> SymbolInformation.Function
| HackSearchService.Typedef -> SymbolInformation.Class
(* LSP doesn't have typedef, so we approximate with class *)
| HackSearchService.Constant -> SymbolInformation.Constant
in
let hack_to_lsp_container = function
| HackSearchService.Method (_, scope) -> Some scope
| HackSearchService.ClassVar (_, scope) -> Some scope
| _ -> None
in
(* Hack sometimes gives us back items with an empty path, by which it *)
(* intends "whichever path you asked me about". That would be meaningless *)
(* here. If it does, then it'll pick up our default path (also empty), *)
(* which will throw and go into our telemetry. That's the best we can do. *)
let hack_symbol_to_lsp (symbol: HackSearchService.symbol) =
{ SymbolInformation.
name = (Utils.strip_ns symbol.name);
kind = hack_to_lsp_kind symbol.result_type;
location = hack_pos_to_lsp_location symbol.pos ~default_path:"";
containerName = hack_to_lsp_container symbol.result_type;
}
in
List.map results ~f:hack_symbol_to_lsp
let do_documentSymbol
(conn: server_conn)
(params: DocumentSymbol.params)
: DocumentSymbol.result =
let open DocumentSymbol in
let open TextDocumentIdentifier in
let open SymbolDefinition in
let filename = lsp_uri_to_path params.textDocument.uri in
let command = ServerCommandTypes.OUTLINE filename in
let results = rpc conn command in
let hack_to_lsp_kind = function
| SymbolDefinition.Function -> SymbolInformation.Function
| SymbolDefinition.Class -> SymbolInformation.Class
| SymbolDefinition.Method -> SymbolInformation.Method
| SymbolDefinition.Property -> SymbolInformation.Property
| SymbolDefinition.Const -> SymbolInformation.Constant
| SymbolDefinition.Enum -> SymbolInformation.Enum
| SymbolDefinition.Interface -> SymbolInformation.Interface
| SymbolDefinition.Trait -> SymbolInformation.Interface
(* LSP doesn't have traits, so we approximate with interface *)
| SymbolDefinition.LocalVar -> SymbolInformation.Variable
| SymbolDefinition.Typeconst -> SymbolInformation.Class
(* e.g. "const type Ta = string;" -- absent from LSP *)
| SymbolDefinition.Typedef -> SymbolInformation.Class
(* e.g. top level type alias -- absent from LSP *)
| SymbolDefinition.Param -> SymbolInformation.Variable
(* We never return a param from a document-symbol-search *)
in
let hack_symbol_to_lsp definition containerName =
{ SymbolInformation.
name = definition.name;
kind = hack_to_lsp_kind definition.kind;
location = hack_symbol_definition_to_lsp_location definition ~default_path:filename;
containerName;
}
in
let rec hack_symbol_tree_to_lsp ~accu ~container_name = function
(* Flattens the recursive list of symbols *)
| [] -> List.rev accu
| def :: defs ->
let children = Option.value def.children ~default:[] in
let accu = (hack_symbol_to_lsp def container_name) :: accu in
let accu = hack_symbol_tree_to_lsp accu (Some def.name) children in
hack_symbol_tree_to_lsp accu container_name defs
in
hack_symbol_tree_to_lsp ~accu:[] ~container_name:None results
let do_findReferences
(conn: server_conn)
(params: FindReferences.params)
: FindReferences.result =
let open FindReferences in
let {Ide_api_types.line; column;} = lsp_position_to_ide params.position in
let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument in
let include_defs = params.context.includeDeclaration in
let command = ServerCommandTypes.IDE_FIND_REFS
(ServerUtils.FileName filename, line, column, include_defs) in
let results = rpc conn command in
(* TODO: respect params.context.include_declaration *)
match results with
| None -> []
| Some (_name, positions) ->
List.map positions ~f:(hack_pos_to_lsp_location ~default_path:filename)
let do_documentHighlights
(conn: server_conn)
(params: DocumentHighlights.params)
: DocumentHighlights.result =
let open DocumentHighlights in
let (file, line, column) = lsp_file_position_to_hack params in
let command = ServerCommandTypes.IDE_HIGHLIGHT_REFS (ServerUtils.FileName file, line, column) in
let results = rpc conn command in
let hack_range_to_lsp_highlight range =
{
range = ide_range_to_lsp range;
kind = None;
}
in
List.map results ~f:hack_range_to_lsp_highlight
let do_typeCoverage (conn: server_conn) (params: TypeCoverage.params)
: TypeCoverage.result =
let open TypeCoverage in
let filename = Lsp_helpers.lsp_textDocumentIdentifier_to_filename params.textDocument in
let command = ServerCommandTypes.COVERAGE_LEVELS (ServerUtils.FileName filename) in
let results: Coverage_level.result = rpc conn command in
let results = Coverage_level.merge_adjacent_results results in
(* We want to get a percentage-covered number. We could do that with an *)
(* additional server round trip to ServerCommandTypes.COVERAGE_COUNTS. *)
(* But to avoid that, we'll instead use this rough approximation: *)
(* Count how many checked/unchecked/partial "regions" there are, where *)
(* a "region" is like results_merged, but counting each line separately. *)
let count_region (nchecked, nunchecked, npartial) (pos, level) =
let nlines = (Pos.end_line pos) - (Pos.line pos) + 1 in
match level with
| Ide_api_types.Checked -> (nchecked + nlines, nunchecked, npartial)
| Ide_api_types.Unchecked -> (nchecked, nunchecked + nlines, npartial)
| Ide_api_types.Partial -> (nchecked, nunchecked, npartial + nlines)
in
let (nchecked, nunchecked, npartial) =
List.fold results ~init:(0,0,0) ~f:count_region in
let ntotal = nchecked + nunchecked + npartial in
let coveredPercent = if ntotal = 0 then 100
else ((nchecked * 100) + (npartial * 50)) / ntotal in
let hack_coverage_to_lsp (pos, level) =
let range = hack_pos_to_lsp_range pos in
match level with
| Ide_api_types.Checked -> None
| Ide_api_types.Unchecked -> Some
{ range;
message = "Un-type checked code. Consider adding type annotations.";
}
| Ide_api_types.Partial -> Some
{ range;
message = "Partially type checked code. Consider adding type annotations.";
}
in
{
coveredPercent;
uncoveredRanges = List.filter_map results ~f:hack_coverage_to_lsp;
}
let do_formatting_common
(conn: server_conn)
(args: ServerFormatTypes.ide_action)
: TextEdit.t list =
let open ServerFormatTypes in
let command = ServerCommandTypes.IDE_FORMAT args in
let response: ServerFormatTypes.ide_result = rpc conn command in
match response with
| Error message ->
raise (Error.InternalError message)
| Ok r ->
let range = ide_range_to_lsp r.range in
let newText = r.new_text in
[{TextEdit.range; newText;}]
let do_documentRangeFormatting
(conn: server_conn)
(params: DocumentRangeFormatting.params)
: DocumentRangeFormatting.result =
let open DocumentRangeFormatting in
let open TextDocumentIdentifier in
let action = ServerFormatTypes.Range
{ Ide_api_types.
range_filename = lsp_uri_to_path params.textDocument.uri;
file_range = lsp_range_to_ide params.range;
}
in
do_formatting_common conn action
let do_documentOnTypeFormatting
(conn: server_conn)
(params: DocumentOnTypeFormatting.params)
: DocumentOnTypeFormatting.result =
let open DocumentOnTypeFormatting in
let open TextDocumentIdentifier in
let action = ServerFormatTypes.Position
{ Ide_api_types.
filename = lsp_uri_to_path params.textDocument.uri;
position = lsp_position_to_ide params.position;
} in
do_formatting_common conn action
let do_documentFormatting
(conn: server_conn)
(params: DocumentFormatting.params)
: DocumentFormatting.result =
let open DocumentFormatting in
let open TextDocumentIdentifier in
let action = ServerFormatTypes.Document (lsp_uri_to_path params.textDocument.uri) in
do_formatting_common conn action
(* do_server_busy: controls the progress / action-required indicator *)
let do_server_busy (state: state) (status: ServerCommandTypes.busy_status) : state =
let open ServerCommandTypes in
let open Main_env in
let (progress, action) = match status with
| Needs_local_typecheck -> (Some "Hack: preparing to check edits", None)
| Doing_local_typecheck -> (Some "Hack: checking edits", None)
| Done_local_typecheck -> (None, Some "Hack: save any file to do a whole-program check")
| Doing_global_typecheck -> (Some "Hack: checking entire project", None)
| Done_global_typecheck -> (None, None)
in
(* Following code is subtle. Thanks to the magic of the notify_ functions, *)
(* it will either create a new progress/action notification, or update an *)
(* an existing one, or close an existing one, or just no-op, as appropriate *)
match state with
| Main_loop menv ->
Main_loop { menv with
progress = Lsp_helpers.notify_progress to_stdout menv.progress progress;
actionRequired = Lsp_helpers.notify_actionRequired to_stdout menv.actionRequired action;
}
| _ ->
state
(* do_diagnostics: sends notifications for all reported diagnostics; also *)
(* returns an updated "files_with_diagnostics" set of all files for which *)
(* our client currently has non-empty diagnostic reports. *)
let do_diagnostics
(uris_with_diagnostics: SSet.t)
(file_reports: Pos.absolute Errors.error_ list SMap.t)
: SSet.t =
(* Hack sometimes reports a diagnostic on an empty file when it can't *)
(* figure out which file to report. In this case we'll report on the root. *)
(* Nuclide and VSCode both display this fine, though they obviously don't *)
(* let you click-to-go-to-file on it. *)
let default_path = match get_root () with
| None -> failwith "expected root"
| Some root -> Path.to_string root in
let file_reports = match SMap.get "" file_reports with
| None -> file_reports
| Some errors -> SMap.remove "" file_reports |> SMap.add ~combine:(@) default_path errors
in
let per_file file errors =
hack_errors_to_lsp_diagnostic file errors
|> print_diagnostics
|> Jsonrpc.notify to_stdout "textDocument/publishDiagnostics"
in
SMap.iter per_file file_reports;
let is_error_free _uri errors = List.is_empty errors in
(* reports_without/reports_with are maps of filename->ErrorList. *)
let (reports_without, reports_with) = SMap.partition is_error_free file_reports in
(* files_without/files_with are sets of filenames *)
let files_without = SMap.bindings reports_without |> List.map ~f:fst in
let files_with = SMap.bindings reports_with |> List.map ~f:fst in
(* uris_without/uris_with are sets of uris *)
let uris_without = List.map files_without ~f:(path_to_lsp_uri ~default_path) |> SSet.of_list in
let uris_with = List.map files_with ~f:(path_to_lsp_uri ~default_path) |> SSet.of_list
in
(* this is "(uris_with_diagnostics \ uris_without) U uris_with" *)
SSet.union (SSet.diff uris_with_diagnostics uris_without) uris_with
let report_connect_start
(ienv: In_init_env.t)
: state =
let open In_init_env in
assert (not ienv.has_reported_progress);
assert (ienv.dialog = ShowMessageRequest.None);
assert (ienv.progress = Progress.None);