Source file vcaml_plugin.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
open Core
open Async
open Vcaml
module Private = Vcaml_test_helpers.Private
let behave_nicely_when_called_from_nvim ~stdout_is_used_for_msgpack =
let behave_nicely writer =
Writer.set_buffer_age_limit writer `Unlimited;
Writer.set_raise_when_consumer_leaves writer false
in
behave_nicely (force Writer.stderr);
if not stdout_is_used_for_msgpack then behave_nicely (force Writer.stdout)
;;
let run_with_crash_handler ~on_crash ~f =
match%bind Monitor.try_with_or_error ~rest:(`Call ignore) f with
| Ok () -> return ()
| Error error ->
let%map () =
Monitor.try_with ~rest:(`Call ignore) (fun () -> on_crash error)
|> Deferred.ignore_m
in
Error.raise error
;;
module Oneshot = struct
module Rpc = struct
type t =
| Blocking_rpc :
{ here : Source_code_position.t
; name : string
; type_ : 'fn Ocaml_from_nvim.Blocking.t
; f : client:[ `blocking ] Client.t -> 'fn
}
-> t
let create ~(here : [%call_pos]) name ~type_ ~f =
Blocking_rpc { here; name; type_; f }
;;
end
let create ?on_crash ~name ~description rpcs =
let f () =
behave_nicely_when_called_from_nvim ~stdout_is_used_for_msgpack:true;
let client = Client.create ~name ~on_error:`Raise in
let shutdown = Ivar.create () in
let invoked_rpc = Set_once.create () in
let oneshot ~name f =
match Set_once.get invoked_rpc with
| Some previously_invoked ->
Deferred.Or_error.error_s
[%message
"Already invoked an RPC"
~invoking:(name : string)
(previously_invoked : string)]
| None ->
Set_once.set_exn invoked_rpc name;
f ()
in
List.iter rpcs ~f:(fun (Rpc.Blocking_rpc { here; name; type_; f }) ->
Ocaml_from_nvim.Private.register_request_blocking
~here
(Not_connected client)
~name
~type_
~f:(fun () ~run_in_background ->
run_in_background ~here (fun client ->
let%map.Deferred.Or_error (_ : Buffer.t) =
Nvim.get_current_buf client
in
Ivar.fill_exn shutdown ());
f)
~wrap_f:(oneshot ~name));
let%bind (_ : [ `asynchronous ] Client.t) =
Private.attach_client client Stdio >>| ok_exn
in
Ivar.read shutdown
in
Async.Command.async
~behave_nicely_in_pipeline:false
~summary:description
(Async.Command.Param.return (fun () ->
match on_crash with
| None -> f ()
| Some on_crash -> run_with_crash_handler ~on_crash ~f))
;;
end
module Persistent = struct
module Rpc = struct
type 'state t =
| Blocking_rpc :
{ here : Source_code_position.t
; name : string
; type_ : 'fn Ocaml_from_nvim.Blocking.t
; f :
'state
-> run_in_background:
(here:[%call_pos]
-> ([ `asynchronous ] Client.t -> unit Deferred.Or_error.t)
-> unit)
-> client:[ `blocking ] Client.t
-> 'fn
; on_keyboard_interrupt : (unit -> unit) option
}
-> 'state t
| Async_rpc :
{ here : Source_code_position.t
; name : string
; type_ : 'fn Ocaml_from_nvim.Async.t
; f : 'state -> client:[ `asynchronous ] Client.t -> 'fn
}
-> 'state t
let create_blocking ?on_keyboard_interrupt ~(here : [%call_pos]) name ~type_ ~f =
Blocking_rpc { here; name; type_; f; on_keyboard_interrupt }
;;
let create_async ~(here : [%call_pos]) name ~type_ ~f =
Async_rpc { here; name; type_; f }
;;
let contra_map t ~f =
match t with
| Blocking_rpc { here; name; type_; f = f'; on_keyboard_interrupt } ->
Blocking_rpc { here; name; type_; f = Fn.compose f' f; on_keyboard_interrupt }
| Async_rpc { here; name; type_; f = f' } ->
Async_rpc { here; name; type_; f = Fn.compose f' f }
;;
end
let register_handlers ~client ~state rpcs =
let get_state state ~name =
match Set_once.get state with
| Some state -> state
| None ->
raise_s [%message "Called RPC before plugin finished initializing." name]
in
let client = Client.Maybe_connected.Not_connected client in
List.iter rpcs ~f:(function
| Rpc.Blocking_rpc { here; name; type_; f; on_keyboard_interrupt } ->
Ocaml_from_nvim.Private.register_request_blocking
?on_keyboard_interrupt
~here
client
~name
~type_
~f
~wrap_f:(fun f -> f (get_state state ~name))
| Async_rpc { here; name; type_; f } ->
Ocaml_from_nvim.Private.register_request_async
~here
client
~name
~type_
~f
~wrap_f:(fun f -> f (get_state state ~name)))
;;
let create'
?on_crash
?after_startup
~name
~description
~param
~on_startup
~notify_fn
rpcs
=
let f ~param =
behave_nicely_when_called_from_nvim ~stdout_is_used_for_msgpack:false;
let state = Set_once.create () in
let display_error_in_neovim' =
ref (fun ~(here : [%call_pos]) _ ->
let _ : Source_code_position.t = here in
())
in
let on_error =
let failed_to_parse_ui_event = ref false in
let failed_to_parse_buffer_event = ref false in
fun (error : Vcaml_error.t) ->
match error with
| Msgpack_rpc_error error -> error |> Msgpack_rpc.Error.to_error |> Error.raise
| Nvim_error_event error_event ->
!display_error_in_neovim' (Vcaml_error.Nvim_error_event.to_error error_event)
| Nvim_error_event_parse_failure notification ->
!display_error_in_neovim'
(Error.create_s
[%message
"Failed to parse error event"
~_:(notification : Vcaml_error.Notification.t)])
| Nvim_buffer_event_parse_failure (exn, notification) ->
(match !failed_to_parse_buffer_event with
| true -> ()
| false ->
failed_to_parse_buffer_event := true;
!display_error_in_neovim'
(Error.create_s
[%message
"Failed to parse buffer event(s) - only reporting first failure."
~error:(exn : exn)
(notification : Vcaml_error.Notification.t)]))
| Nvim_ui_event_parse_failure (exn, notification) ->
(match !failed_to_parse_ui_event with
| true -> ()
| false ->
failed_to_parse_ui_event := true;
!display_error_in_neovim'
(Error.create_s
[%message
"Failed to parse UI event(s) - only reporting first failure."
~error:(exn : exn)
(notification : Vcaml_error.Notification.t)]))
in
let client = Client.create ~name ~on_error:(`Call on_error) in
let rpc_registration_failure = ref None in
(try register_handlers ~client ~state rpcs with
| exn -> rpc_registration_failure := Some exn);
let%bind client =
Private.attach_client client (Socket `Infer_from_parent_nvim) >>| ok_exn
in
(display_error_in_neovim'
:= fun ~(here : [%call_pos]) error ->
Private.notify_nvim_of_error client ~here error |> don't_wait_for);
let%bind () =
match !rpc_registration_failure with
| None -> return ()
| Some exn ->
let%bind () = Private.notify_nvim_of_error client (Error.of_exn exn) in
raise exn
in
match%bind Monitor.try_with_join_or_error (fun () -> on_startup param ~client) with
| Error error ->
let%bind () = Private.notify_nvim_of_error client error in
Error.raise error
| Ok state' ->
Set_once.set_exn state state';
(match%bind
match after_startup with
| None -> Deferred.Or_error.return ()
| Some f -> Monitor.try_with_join_or_error (fun () -> f state' ~client)
with
| Error error ->
let%bind () = Private.notify_nvim_of_error client error in
Error.raise error
| Ok () ->
let%bind () =
match%bind
Nvim.call_function
client
~name:notify_fn
~type_:Nvim.Func.(Int @-> return Object)
(Client.channel client)
with
| Error error ->
let%bind () = Private.notify_nvim_of_error client error in
Error.raise error
| Ok value ->
(match value, notify_fn with
| Int 0, `Viml _ | Nil, `Lua _ -> return ()
| _, (`Viml function_name | `Lua function_name) ->
let error =
Error.create_s
[%message
(sprintf "%s returned a value" function_name)
~_:(value : Msgpack.t)]
in
let%bind () = Private.notify_nvim_of_error client error in
Error.raise error)
in
Deferred.never ())
in
Async.Command.async
~behave_nicely_in_pipeline:false
~summary:description
(let%map_open.Async.Command param in
fun () ->
match on_crash with
| None -> f ~param
| Some on_crash -> run_with_crash_handler ~on_crash ~f:(fun () -> f ~param))
;;
let create ?on_crash ?after_startup ~name ~description ~on_startup ~notify_fn rpcs =
create'
?on_crash
?after_startup
~name
~description
~param:(Async.Command.Param.return ())
~on_startup:(fun () ~client -> on_startup client)
~notify_fn
rpcs
;;
end