package vcaml

  1. Overview
  2. Docs

Source file buf.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
open Core
open Async

type t = Types.Buf.t

let sexp_of_t t =
  Sexp.(
    List
      [ Atom "Buffer"; Nvim_internal.Types.Buffer.to_msgpack t |> [%sexp_of: Msgpack.t] ])
;;

module Table = Types.Buf.Table

type mark =
  { row : int
  ; col : int
  }

type which_buffer =
  [ `Current
  | `Numbered of t
  ]

open Types

let to_msgpack = Types.Buf.to_msgpack
let of_msgpack = Types.Buf.of_msgpack

(* We can't use [Vcaml.Client.eval] because of dependency cycles

   This function is pretty fragile, but there isn't really a better way to know what
   buffer we're currently in, so we pretty much have to do this to get good filtering
   from [`Current] event listen requests.
*)
let current_buffer =
  let query : Msgpack.t Nvim_internal.Types.api_result =
    { name = "nvim_eval"
    ; params = Array [ String "bufnr(\"%\")" ]
    ; witness = Nvim_internal.Types.Phantom.Object
    }
  in
  let open Api_call.Let_syntax in
  let%map result = Api_call.of_api_result query in
  Or_error.bind ~f:Nvim_internal.Types.Buffer.of_msgpack result
;;

module Event = struct
  type nonrec t =
    | Lines of
        { buffer : t
        ; changedtick : int option
        ; firstline : int
        ; lastline : int
        ; linedata : string list
        ; more : bool
        }
    | Changed_tick of
        { buffer : t
        ; changedtick : int
        }
    | Detach of t
  [@@deriving sexp_of]

  let parse { Msgpack_rpc.method_name; params } =
    let open Option.Let_syntax in
    match method_name with
    | "nvim_buf_lines_event" ->
      (match params with
       | [ (Extension _ as buf)
         ; changedtick
         ; Integer firstline
         ; Integer lastline
         ; Array changes
         ; Boolean more
         ] ->
         let%bind linedata =
           Option.try_with (fun () ->
             List.map changes ~f:(function
               | String s -> s
               | _ -> failwith "short-circuit"))
         in
         let%bind buffer = of_msgpack buf |> Or_error.ok in
         let%bind changedtick =
           match changedtick with
           | Nil -> Some None
           | Integer i -> Some (Some i)
           | _ -> None
         in
         Some (Lines { buffer; changedtick; firstline; lastline; linedata; more })
       | _ -> None)
    | "nvim_buf_changedtick_event" ->
      (match params with
       | [ (Extension _ as buf); Integer changedtick ] ->
         let%bind buffer = of_msgpack buf |> Or_error.ok in
         Some (Changed_tick { buffer; changedtick })
       | [ (Extension _ as buf); Nil ] ->
         let%bind buffer = of_msgpack buf |> Or_error.ok in
         Some (Changed_tick { buffer; changedtick = 0 })
       | _ -> None)
    | "nvim_buf_detach_event" ->
      (match params with
       | [ (Extension _ as buf) ] ->
         let%bind buffer = of_msgpack buf |> Or_error.ok in
         Some (Detach buffer)
       | _ -> None)
    | _ -> None
  ;;

  let for_buffer buf =
    let equal = Nvim_internal.Types.Buffer.equal in
    function
    | Lines { buffer; _ } | Changed_tick { buffer; _ } | Detach buffer ->
      equal buffer buf
  ;;
end

open Msgpack

let get_name ~buffer =
  Nvim_internal.Wrappers.nvim_buf_get_name ~buffer |> Api_call.of_api_result
;;

let get_lines ~buffer ~start ~end_ ~strict_indexing =
  let open Api_call.Let_syntax in
  let%map result =
    Nvim_internal.Wrappers.nvim_buf_get_lines ~buffer ~start ~end_ ~strict_indexing
    |> Api_call.of_api_result
  in
  let open Or_error.Let_syntax in
  let%bind result = result in
  Or_error.try_with (fun () ->
    List.map result ~f:(function
      | String s -> s
      | _ -> failwith "malformed result"))
;;

let set_lines ~buffer ~start ~end_ ~strict_indexing ~replacement =
  let replacement = List.map ~f:(fun v -> String v) replacement in
  Nvim_internal.Wrappers.nvim_buf_set_lines
    ~buffer
    ~start
    ~end_
    ~strict_indexing
    ~replacement
  |> Api_call.of_api_result
;;

let buf_events { events; _ } =
  let r, w = Pipe.create () in
  let s =
    Bus.subscribe_exn events [%here] ~f:(fun e ->
      match Event.parse e with
      | Some evt -> Pipe.write_without_pushback_if_open w evt
      | None -> ())
  in
  upon
    (Deferred.any [ Pipe.closed r; Pipe.closed w ])
    (fun () ->
       Pipe.close_read r;
       Pipe.close w;
       Bus.unsubscribe events s);
  r
;;

let attach
      ?(opts = [])
      ({ attach_sequencer; buffers_attached; _ } as cli)
      ~(buffer : which_buffer)
      ~send_buffer
  =
  let buffer_query =
    match buffer with
    | `Current -> Nvim_internal.Types.Buffer.of_msgpack (Integer 0) |> Or_error.ok_exn
    | `Numbered b -> b
  in
  let attach =
    Api_call.of_api_result
      (Nvim_internal.Wrappers.nvim_buf_attach ~buffer:buffer_query ~send_buffer ~opts)
  in
  let curr_bufnr =
    match buffer with
    | `Current -> current_buffer
    | `Numbered b -> Api_call.return (Ok b)
  in
  let call = Api_call.both attach curr_bufnr in
  let open Deferred.Or_error.Let_syntax in
  (* We always run the actual attach because it's possible that the user has deleted the
     buffer (and so we want to fail with an error).
  *)
  let%bind success, bufnr = Api_call.run cli call in
  let run_attach () =
    let open Deferred.Or_error.Let_syntax in
    if%bind Deferred.return success
    then (
      let%bind bufnr = Deferred.return bufnr in
      Hashtbl.change buffers_attached bufnr ~f:(function
        | Some x -> Some (x + 1)
        | None -> Some 1);
      let incoming = Pipe.filter (buf_events cli) ~f:(Event.for_buffer bufnr) in
      let r =
        Pipe.create_reader ~close_on_exception:false (fun w ->
          Pipe.iter incoming ~f:(function
            | Event.Detach _ as evt ->
              let open Deferred.Let_syntax in
              (* Write without pushback here because we don't want the scheduler
                 interrupting us *)
              Pipe.write_without_pushback_if_open w evt;
              Pipe.close w;
              Hashtbl.remove buffers_attached bufnr;
              return ()
            | evt -> Pipe.write_if_open w evt))
      in
      upon (Pipe.closed r) (fun () ->
        upon
          (Api_call.run_join
             cli
             (Nvim_internal.Wrappers.nvim_buf_detach ~buffer:bufnr
              |> Api_call.of_api_result))
          (function
            | Ok true -> Hashtbl.remove buffers_attached bufnr
            | _ -> Log.Global.error "failed to detach from buffer, ignoring"));
      return r)
    else Deferred.Or_error.error_string "unable to connect to buffer"
  in
  Throttle.enqueue attach_sequencer run_attach
;;

module Untested = struct
  let line_count ~buffer =
    Api_call.of_api_result (Nvim_internal.Wrappers.nvim_buf_line_count ~buffer)
  ;;

  let get_var ~buffer ~name =
    Nvim_internal.Wrappers.nvim_buf_get_var ~buffer ~name |> Api_call.of_api_result
  ;;

  let get_changedtick ~buffer =
    Nvim_internal.Wrappers.nvim_buf_get_changedtick ~buffer |> Api_call.of_api_result
  ;;

  let get_keymap ~buffer ~mode =
    let open Api_call.Let_syntax in
    let%map result =
      Nvim_internal.Wrappers.nvim_buf_get_keymap ~buffer ~mode |> Api_call.of_api_result
    in
    let open Or_error.Let_syntax in
    let%bind result = result in
    List.map ~f:(Keymap.Untested.of_msgpack ~to_buf:of_msgpack) result
    |> Or_error.combine_errors
  ;;

  let get_commands ?(opts = []) ~buffer =
    let open Api_call.Let_syntax in
    let%map result =
      Nvim_internal.Wrappers.nvim_buf_get_commands ~buffer ~opts
      |> Api_call.of_api_result
    in
    let open Or_error.Let_syntax in
    let%bind result = result in
    let%bind commands_with_names =
      List.map result ~f:(fun (name, command) ->
        let open Or_error.Let_syntax in
        let%bind n = Extract.string name in
        let%bind c = Nvim_command.of_msgpack command in
        return (n, c))
      |> Or_error.combine_errors
    in
    String.Map.of_alist_or_error commands_with_names
  ;;

  let set_var ~buffer ~name ~value =
    Nvim_internal.Wrappers.nvim_buf_set_var ~buffer ~name ~value
    |> Api_call.of_api_result
  ;;

  let del_var ~buffer ~name =
    Nvim_internal.Wrappers.nvim_buf_del_var ~buffer ~name |> Api_call.of_api_result
  ;;

  let get_option ~buffer ~name =
    Nvim_internal.Wrappers.nvim_buf_get_option ~buffer ~name |> Api_call.of_api_result
  ;;

  let set_option ~buffer ~name ~value =
    Nvim_internal.Wrappers.nvim_buf_set_option ~buffer ~name ~value
    |> Api_call.of_api_result
  ;;

  let set_name ~buffer ~name =
    Nvim_internal.Wrappers.nvim_buf_set_name ~buffer ~name |> Api_call.of_api_result
  ;;

  let is_valid ~buffer =
    Nvim_internal.Wrappers.nvim_buf_is_valid ~buffer |> Api_call.of_api_result
  ;;

  let get_mark ~buffer ~name =
    let open Api_call.Let_syntax in
    let%map pos =
      Nvim_internal.Wrappers.nvim_buf_get_mark ~buffer ~name |> Api_call.of_api_result
    in
    let open Or_error.Let_syntax in
    match%bind pos with
    | [ Integer row; Integer col ] -> Ok { row; col }
    | _ -> Or_error.error_string "malformed result from [nvim_buf_get_mark]"
  ;;

  let add_highlight ~buffer ~ns_id ~hl_group ~line ~col_start ~col_end =
    Nvim_internal.Wrappers.nvim_buf_add_highlight
      ~buffer
      ~ns_id
      ~hl_group
      ~line
      ~col_start
      ~col_end
    |> Api_call.of_api_result
  ;;

  let clear_highlight ~buffer ~ns_id ~line_start ~line_end =
    Nvim_internal.Wrappers.nvim_buf_clear_highlight ~buffer ~ns_id ~line_start ~line_end
    |> Api_call.of_api_result
  ;;

  let find_by_name_or_create ~name =
    let open Api_call.Let_syntax in
    let%map result =
      Nvim_internal.Wrappers.nvim_eval ~expr:(sprintf "bufnr(\"%s\", 1)" name)
      |> Api_call.of_api_result
    in
    Or_error.bind ~f:Nvim_internal.Types.Buffer.of_msgpack result
  ;;

  let set_scratch ~buffer =
    let open Api_call.Let_syntax in
    let%map _nofile = set_option ~buffer ~name:"buftype" ~value:(String "nofile")
    and _bufhidden = set_option ~buffer ~name:"bufhidden" ~value:(String "hide")
    and _noswapfile = set_option ~buffer ~name:"swapfile" ~value:(Boolean false)
    and _unlisted = set_option ~buffer ~name:"buflisted" ~value:(Boolean false) in
    ()
  ;;
end