-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Expand file tree
/
Copy pathlsp_helpers.ml
More file actions
426 lines (381 loc) · 16.6 KB
/
lsp_helpers.ml
File metadata and controls
426 lines (381 loc) · 16.6 KB
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
(* A few helpful wrappers around LSP *)
open Hh_prelude
open Lsp
open Lsp_fmt
let progress_and_actionRequired_counter = ref 0
(************************************************************************)
(* Conversions *)
(************************************************************************)
let url_scheme_regex = Str.regexp "^\\([a-zA-Z][a-zA-Z0-9+.-]+\\):"
(* this requires schemes with 2+ characters, so "c:\path" isn't considered a scheme *)
let lsp_uri_to_path (uri : DocumentUri.t) : string =
let uri = string_of_uri uri in
if Str.string_match url_scheme_regex uri 0 then
let scheme = Str.matched_group 1 uri in
if String.equal scheme "file" then
File_url.parse uri
else
raise
(Error.LspException
{
Error.code = Error.InvalidParams;
message = Printf.sprintf "Not a valid file url '%s'" uri;
data = None;
})
else
uri
let path_string_to_lsp_uri (path : string) ~(default_path : string) :
Lsp.DocumentUri.t =
if String.equal path "" then begin
HackEventLogger.invariant_violation_bug "missing path";
Hh_logger.log
"missing path %s"
(Exception.get_current_callstack_string 99 |> Exception.clean_stack);
File_url.create default_path |> uri_of_string
end else
File_url.create path |> uri_of_string
let path_to_lsp_uri (path : Relative_path.t) : Lsp.DocumentUri.t =
match Relative_path.prefix path with
| Relative_path.(Root | Hhi | Tmp) ->
let absolute = Relative_path.to_absolute path in
path_string_to_lsp_uri absolute ~default_path:absolute
| Relative_path.Dummy ->
(* This is bad, we're saying something is a file path when it's not.
A particular case where we reach here is via `hh_single_type_check --ide-code-actions.
This is a symptom of overusing LSP types in internals of our language server.
TODO(T168350458): try to make this unreachable or behave more reasonably
*)
Lsp.DocumentUri.Uri (Relative_path.to_absolute path)
let lsp_textDocumentIdentifier_to_filename
(identifier : Lsp.TextDocumentIdentifier.t) : string =
Lsp.TextDocumentIdentifier.(lsp_uri_to_path identifier.uri)
let lsp_position_to_fc ({ Lsp.line; character } : Lsp.position) :
File_content.Position.t =
File_content.Position.from_zero_based line character
let lsp_range_to_fc (range : Lsp.range) : File_content.range =
{
File_content.st = lsp_position_to_fc range.Lsp.start;
ed = lsp_position_to_fc range.Lsp.end_;
}
let lsp_range_to_pos ~line_to_offset path (range : Lsp.range) : Pos.t =
let triple_of_endpoint Lsp.{ line; character } =
let bol = line_to_offset line in
(line, bol, bol + character)
in
Pos.make_from_lnum_bol_offset
~pos_file:path
~pos_start:(triple_of_endpoint range.Lsp.start)
~pos_end:(triple_of_endpoint range.Lsp.end_)
let lsp_edit_to_fc (edit : Lsp.DidChange.textDocumentContentChangeEvent) :
File_content.text_edit =
{
File_content.range = Option.map edit.DidChange.range ~f:lsp_range_to_fc;
text = edit.DidChange.text;
}
let apply_changes
(text : string)
(contentChanges : DidChange.textDocumentContentChangeEvent list) :
(string, string * Exception.t) result =
let edits = List.map ~f:lsp_edit_to_fc contentChanges in
File_content.edit_file text edits
let get_char_from_lsp_position (content : string) (position : Lsp.position) :
char =
let fc_position = lsp_position_to_fc position in
File_content.(get_char content (get_offset content fc_position))
let apply_changes_unsafe
text (contentChanges : DidChange.textDocumentContentChangeEvent list) :
string =
match apply_changes text contentChanges with
| Ok r -> r
| Error (e, _stack) -> failwith e
let sym_occ_kind_to_lsp_sym_info_kind (sym_occ_kind : SymbolOccurrence.kind) :
Lsp.SymbolInformation.symbolKind =
let open Lsp.SymbolInformation in
let open SymbolOccurrence in
match sym_occ_kind with
| Class _ -> Class
| BuiltInType _ -> Class
| Function -> Function
| Method _ -> Method
| LocalVar -> Variable
| TypeVar -> TypeParameter
| Property _ -> Property
| XhpLiteralAttr _ -> Property
| ClassConst _ -> TypeParameter
| Typeconst _ -> TypeParameter
| GConst -> Constant
| Attribute _ -> Class
| EnumClassLabel _ -> EnumMember
| Keyword _ -> Null
| PureFunctionContext -> Null
| BestEffortArgument _ -> Null
| HhFixme -> Null
| HhIgnore -> Null
| Module -> Module
let hack_pos_to_lsp_range ~(equal : 'a -> 'a -> bool) (pos : 'a Pos.pos) :
Lsp.range =
(* .hhconfig errors are Positions with a filename, but dummy start/end
* positions. Handle that case - and Pos.none - specially, as the LSP
* specification requires line and character >= 0, and VSCode silently
* drops diagnostics that violate the spec in this way *)
if Pos.equal_pos equal pos (Pos.make_from (Pos.filename pos)) then
{ start = { line = 0; character = 0 }; end_ = { line = 0; character = 0 } }
else
let (line1, col1, line2, col2) = Pos.destruct_range_one_based pos in
{
start = { line = line1 - 1; character = col1 - 1 };
end_ = { line = line2 - 1; character = col2 - 1 };
}
let hack_pos_to_lsp_range_adjusted (p : 'a Pos.pos) : range =
let (line_start, line_end, character_start, character_end) =
Pos.info_pos_extended p
in
let (start_position : position) =
{ line = line_start; character = character_start }
in
let (end_position : position) =
{ line = line_end; character = character_end }
in
{ start = start_position; end_ = end_position }
(************************************************************************)
(* Range calculations *)
(************************************************************************)
(* We need to do intersection and other calculations on ranges.
* The functions in the following module all assume LSP 0-based ranges,
* and assume without testing that a range's start is equal to or before
* its end. *)
let pos_compare (p1 : position) (p2 : position) : int =
if p1.line < p2.line then
-1
else if p1.line > p2.line then
1
else
p1.character - p2.character
(* Given a "selection" range A..B and a "squiggle" range a..b, how do they overlap?
* There are 12 ways to order the four letters ABab, of which six
* satisfy both A<=B and a<=b. Here they are. *)
type range_overlap =
| Selection_before_start_of_squiggle (* ABab *)
| Selection_overlaps_start_of_squiggle (* AaBb *)
| Selection_covers_whole_squiggle (* AabB *)
| Selection_in_middle_of_squiggle (* aABb *)
| Selection_overlaps_end_of_squiggle (* aAbB *)
(* abAB *)
| Selection_after_end_of_squiggle
(* Computes how two ranges "selection" and "squiggle" overlap *)
let get_range_overlap (selection : range) (squiggle : range) : range_overlap =
let selStart_leq_squiggleStart =
pos_compare selection.start squiggle.start <= 0
in
let selStart_leq_squiggleEnd =
pos_compare selection.start squiggle.end_ <= 0
in
let selEnd_lt_squiggleStart = pos_compare selection.end_ squiggle.start < 0 in
let selEnd_lt_squiggleEnd = pos_compare selection.end_ squiggle.end_ < 0 in
(* Q. Why does it test "<=" for the first two and "<" for the last two? *)
(* Intuitively you can trust that it has something to do with how ranges are *)
(* inclusive at their start and exclusive at their end. But the real reason *)
(* is just that I did an exhaustive case analysis to look at all cases where *)
(* A,B,a,b might be equal, and decided which outcome I wanted for each of them *)
(* because of how I'm going to treat them in other functions, and retrofitted *)
(* those answers into this function. For instance, if squiggleStart==selEnd, *)
(* I'll want to handle it in the same way as squiggleStart<selEnd<squiggleEnd. *)
(* The choices of "leq" and "lt" in this function embody those answers. *)
match
( selStart_leq_squiggleStart,
selStart_leq_squiggleEnd,
selEnd_lt_squiggleStart,
selEnd_lt_squiggleEnd )
with
| (true, true, true, true) -> Selection_before_start_of_squiggle
| (true, true, false, true) -> Selection_overlaps_start_of_squiggle
| (true, true, false, false) -> Selection_covers_whole_squiggle
| (false, true, false, true) -> Selection_in_middle_of_squiggle
| (false, true, false, false) -> Selection_overlaps_end_of_squiggle
| (false, false, false, false) -> Selection_after_end_of_squiggle
| (true, false, _, _) ->
failwith "sel.start proves squiggle.start > squiggle.end_"
| (_, _, true, false) ->
failwith "sel.end proves squiggle.start > squiggle.end_"
| (false, _, true, _) -> failwith "squiggle.start proves sel.start > sel.end_"
| (_, false, _, true) -> failwith "squiggle.end_ proves sel.start > sel.end_"
(* this structure models a change where a certain range is replaced with
* a block of text. For instance, if you merely insert a single character,
* then remove_range.start==remove_range.end_ and insert_lines=0
* and insert_chars_on_final_line=1. *)
type range_replace = {
remove_range: range;
insert_lines: int;
insert_chars_on_final_line: int;
}
(* If you have a position "p", and some range before this point is replaced with
* text of a certain number of lines, the last line having a certain number of characters,
* then how will the position be shifted?
* Note: this function assumes but doesn't verify that the range ends on or before
* the position. *)
let update_pos_due_to_prior_replace (p : position) (replace : range_replace) :
position =
if replace.remove_range.end_.line < p.line then
(* The replaced range doesn't touch the position, so position merely gets shifted up/down *)
let line =
p.line
- (replace.remove_range.end_.line - replace.remove_range.start.line)
+ replace.insert_lines
in
{ p with line }
else if replace.insert_lines > 0 then
(* The position is on the final line and multiple lines were inserted *)
let line =
p.line
- (replace.remove_range.end_.line - replace.remove_range.start.line)
+ replace.insert_lines
in
let character =
replace.insert_chars_on_final_line
+ (p.character - replace.remove_range.end_.character)
in
{ line; character }
else
(* The position is on the line where a few characters were inserted *)
let line =
p.line - (replace.remove_range.end_.line - replace.remove_range.start.line)
in
let character =
replace.remove_range.start.character
+ replace.insert_chars_on_final_line
+ (p.character - replace.remove_range.end_.character)
in
{ line; character }
(* If you have a squiggle, and some range in the document is replaced with a block
* some lines long and with insert_chars on the final line, then what's the new
* range of the squiggle? *)
let update_range_due_to_replace (squiggle : range) (replace : range_replace) :
range option =
match get_range_overlap replace.remove_range squiggle with
| Selection_before_start_of_squiggle ->
let start = update_pos_due_to_prior_replace squiggle.start replace in
let end_ = update_pos_due_to_prior_replace squiggle.end_ replace in
Some { start; end_ }
| Selection_overlaps_start_of_squiggle ->
let line = replace.remove_range.start.line + replace.insert_lines in
let character =
if replace.insert_lines = 0 then
replace.remove_range.start.character
+ replace.insert_chars_on_final_line
else
replace.insert_chars_on_final_line
in
let start = { line; character } in
let end_ = update_pos_due_to_prior_replace squiggle.end_ replace in
Some { start; end_ }
| Selection_covers_whole_squiggle -> None
| Selection_in_middle_of_squiggle ->
let start = squiggle.start in
let end_ = update_pos_due_to_prior_replace squiggle.end_ replace in
Some { start; end_ }
| Selection_overlaps_end_of_squiggle ->
let start = squiggle.start in
let end_ = replace.remove_range.start in
Some { start; end_ }
| Selection_after_end_of_squiggle -> Some squiggle
(* Moves all diagnostics in response to an LSP change.
* The change might insert text before a diagnostic squiggle (so the squiggle
* has to be moved down or to the right); it might delete text before the squiggle;
* it might modify text inside the squiggle; it might replace text that overlaps
* the squiggle in which case the squiggle gets truncated/moved; it might replace
* the squiggle in its entirety in which case the squiggle gets removed.
* Note that an LSP change is actually a set of changes, applied in order. *)
let update_diagnostics_due_to_change
(diagnostics : PublishDiagnostics.diagnostic list)
(change : Lsp.DidChange.params) : PublishDiagnostics.diagnostic list =
PublishDiagnostics.(
let replace_of_change change =
match change.DidChange.range with
| None -> None
| Some remove_range ->
let offset = String.length change.DidChange.text in
let pos =
File_content.offset_to_position change.DidChange.text offset
in
let (insert_lines, insert_chars_on_final_line) =
File_content.Position.line_column_zero_based pos
in
Some { remove_range; insert_lines; insert_chars_on_final_line }
in
let apply_replace diagnostic_opt replace_opt =
match (diagnostic_opt, replace_opt) with
| (Some diagnostic, Some replace) ->
let range = update_range_due_to_replace diagnostic.range replace in
Option.map range ~f:(fun range -> { diagnostic with range })
| _ -> None
in
let replaces =
Base.List.map change.DidChange.contentChanges ~f:replace_of_change
in
let apply_all_replaces diagnostic =
Base.List.fold replaces ~init:(Some diagnostic) ~f:apply_replace
in
Base.List.filter_map diagnostics ~f:apply_all_replaces)
(************************************************************************)
(* Accessors *)
(************************************************************************)
let get_root (p : Lsp.Initialize.params) : string =
Lsp.Initialize.(
match (p.rootUri, p.rootPath) with
| (Some uri, _) -> lsp_uri_to_path uri
| (None, Some path) -> path
| (None, None) -> failwith "Initialize params missing root")
let supports_status (p : Lsp.Initialize.params) : bool =
Lsp.Initialize.(p.client_capabilities.window.status)
let supports_snippets (p : Lsp.Initialize.params) : bool =
Lsp.Initialize.(
p.client_capabilities.textDocument.completion.completionItem.snippetSupport)
let supports_connectionStatus (p : Lsp.Initialize.params) : bool =
Lsp.Initialize.(p.client_capabilities.telemetry.connectionStatus)
(************************************************************************)
(* Wrappers for some LSP methods *)
(************************************************************************)
let telemetry
(writer : Jsonrpc.writer)
(type_ : MessageType.t)
(extras : (string * Yojson.Safe.t) list)
(message : string) : unit =
let params = { LogMessage.type_; message } in
let notification = TelemetryNotification (params, extras) in
notification |> print_lsp_notification |> writer
let telemetry_error
(writer : Jsonrpc.writer)
?(extras : (string * Yojson.Safe.t) list = [])
(message : string) : unit =
telemetry writer MessageType.ErrorMessage extras message
let telemetry_log
(writer : Jsonrpc.writer)
?(extras : (string * Yojson.Safe.t) list = [])
(message : string) : unit =
telemetry writer MessageType.LogMessage extras message
let log (writer : Jsonrpc.writer) (type_ : MessageType.t) (message : string) :
unit =
let params = { LogMessage.type_; message } in
let notification = LogMessageNotification params in
notification |> print_lsp_notification |> writer
let log_error (writer : Jsonrpc.writer) = log writer MessageType.ErrorMessage
let log_warning (writer : Jsonrpc.writer) =
log writer MessageType.WarningMessage
let log_info (writer : Jsonrpc.writer) = log writer MessageType.InfoMessage
let showMessage
(writer : Jsonrpc.writer) (type_ : MessageType.t) (message : string) : unit
=
let params = { ShowMessage.type_; message } in
let notification = ShowMessageNotification params in
notification |> print_lsp_notification |> writer
let showMessage_info (writer : Jsonrpc.writer) =
showMessage writer MessageType.InfoMessage
let showMessage_warning (writer : Jsonrpc.writer) =
showMessage writer MessageType.WarningMessage
let showMessage_error (writer : Jsonrpc.writer) =
showMessage writer MessageType.ErrorMessage
let title_of_command_or_action =
Lsp.CodeAction.(
function
| Command Command.{ title; _ } -> title
| Action { title; _ } -> title)