Source file worker.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
open Merlin_utils
open Std
open Merlin_kernel
module Location = Ocaml_parsing.Location

let sync_get url =
  let open Js_of_ocaml in
  let x = XmlHttpRequest.create () in
  x##.responseType := Js.string "arraybuffer";
  x##_open (Js.string "GET") (Js.string url) Js._false;
  x##send Js.null;
  match x##.status with
  | 200 ->
      Js.Opt.case
        (File.CoerceTo.arrayBuffer x##.response)
        (fun () ->
          Js_of_ocaml.Console.console##log (Js.string "Failed to receive file");
          None)
        (fun b -> Some (Typed_array.String.of_arrayBuffer b))
  | _ -> None

let filename_of_module unit_name =
  Printf.sprintf "%s.cmi" (String.uncapitalize_ascii unit_name)

let reset_dirs () =
  Ocaml_utils.Directory_content_cache.clear ();
  let open Ocaml_utils.Load_path in
  let dirs = get_paths () in
  reset ();
  List.iter ~f:(fun p -> prepend_dir (Dir.create p)) dirs

let add_dynamic_cmis dcs =
  let open Ocaml_typing.Persistent_env.Persistent_signature in
  let old_loader = !load in

  let fetch filename =
    let url = Filename.concat dcs.Protocol.dcs_url filename in
    sync_get url
  in

  List.iter
    ~f:(fun name ->
      let filename = filename_of_module name in
      match fetch (filename_of_module name) with
      | Some content ->
          let name = Filename.(concat "/static/stdlib" filename) in
          Js_of_ocaml.Sys_js.create_file ~name ~content
      | None -> ())
    dcs.dcs_toplevel_modules;

  let new_load ~unit_name =
    let filename = filename_of_module unit_name in
    let fs_name = Filename.(concat "/static/stdlib" filename) in
    (* Check if it's already been downloaded. This will be the
       case for all toplevel cmis. Also check whether we're supposed
       to handle this cmi *)
    (if
       (not (Sys.file_exists fs_name))
       && List.exists
            ~f:(fun prefix -> String.starts_with ~prefix filename)
            dcs.dcs_file_prefixes
     then
       match fetch filename with
       | Some x ->
           Js_of_ocaml.Sys_js.create_file ~name:fs_name ~content:x;
           (* At this point we need to tell merlin that the dir contents
               have changed *)
           reset_dirs ()
       | None ->
           Printf.eprintf "Warning: Expected to find cmi at: %s\n%!"
             (Filename.concat dcs.Protocol.dcs_url filename));
    old_loader ~unit_name
  in
  load := new_load

let add_cmis { Protocol.static_cmis; dynamic_cmis } =
  List.iter static_cmis ~f:(fun { Protocol.sc_name; sc_content } ->
      let filename =
        Printf.sprintf "%s.cmi" (String.uncapitalize_ascii sc_name)
      in
      let name = Filename.(concat "/static/stdlib" filename) in
      Js_of_ocaml.Sys_js.create_file ~name ~content:sc_content);
  Option.iter ~f:add_dynamic_cmis dynamic_cmis;
  Protocol.Added_cmis

let config =
  let initial = Mconfig.initial in
  {
    initial with
    merlin = { initial.merlin with stdlib = Some "/static/stdlib" };
  }

let make_pipeline source = Mpipeline.make config source

let dispatch source query =
  let pipeline = make_pipeline source in
  Mpipeline.with_pipeline pipeline @@ fun () ->
  Query_commands.dispatch pipeline query

module Completion = struct
  (* Prefixing code from ocaml-lsp-server *)
  let rfindi =
    let rec loop s ~f i =
      if i < 0 then None
      else if f (String.unsafe_get s i) then Some i
      else loop s ~f (i - 1)
    in
    fun ?from s ~f ->
      let from =
        let len = String.length s in
        match from with
        | None -> len - 1
        | Some i ->
            if i > len - 1 then raise @@ Invalid_argument "rfindi: invalid from"
            else i
      in
      loop s ~f from

  let lsplit2 s ~on =
    match String.index_opt s on with
    | None -> None
    | Some i ->
        let open String in
        Some (sub s ~pos:0 ~len:i, sub s ~pos:(i + 1) ~len:(length s - i - 1))

  (** @see <https://ocaml.org/manual/lex.html> reference *)
  let prefix_of_position ?(short_path = false) source position =
    match Msource.text source with
    | "" -> ""
    | text ->
        let from =
          let (`Offset index) = Msource.get_offset source position in
          min (String.length text - 1) (index - 1)
        in
        let pos =
          let should_terminate = ref false in
          let has_seen_dot = ref false in
          let is_prefix_char c =
            if !should_terminate then false
            else
              match c with
              | 'a' .. 'z'
              | 'A' .. 'Z'
              | '0' .. '9'
              | '\'' | '_'
              (* Infix function characters *)
              | '$' | '&' | '*' | '+' | '-' | '/' | '=' | '>' | '@' | '^' | '!'
              | '?' | '%' | '<' | ':' | '~' | '#' ->
                  true
              | '`' ->
                  if !has_seen_dot then false
                  else (
                    should_terminate := true;
                    true)
              | '.' ->
                  has_seen_dot := true;
                  not short_path
              | _ -> false
          in
          rfindi text ~from ~f:(fun c -> not (is_prefix_char c))
        in
        let pos = match pos with None -> 0 | Some pos -> pos + 1 in
        let len = from - pos + 1 in
        let reconstructed_prefix = String.sub text ~pos ~len in
        (* if we reconstructed [~f:ignore] or [?f:ignore], we should take only
           [ignore], so: *)
        if
          String.is_prefixed ~by:"~" reconstructed_prefix
          || String.is_prefixed ~by:"?" reconstructed_prefix
        then
          match lsplit2 reconstructed_prefix ~on:':' with
          | Some (_, s) -> s
          | None -> reconstructed_prefix
        else reconstructed_prefix

  let at_pos source position =
    let prefix = prefix_of_position source position in
    let (`Offset to_) = Msource.get_offset source position in
    let from =
      to_ - String.length (prefix_of_position ~short_path:true source position)
    in
    if prefix = "" then None
    else
      let query =
        Query_protocol.Complete_prefix (prefix, position, [], true, true)
      in
      Some (from, to_, dispatch source query)
end
(*
let dump () =
  let query = Query_protocol.Dump [`String "paths"] in
  dispatch (Msource.make "") query *)

(* let dump_config () =
   let pipeline = make_pipeline (Msource.make "") in
   Mpipeline.with_pipeline pipeline @@ fun () ->
     Mconfig.dump (Mpipeline.final_config pipeline)
     |> Json.pretty_to_string *)

let on_message marshaled_message =
  let action : Protocol.action = Marshal.from_bytes marshaled_message 0 in
  let res =
    match action with
    | Complete_prefix (source, position) -> (
        let source = Msource.make source in
        match Completion.at_pos source position with
        | Some (from, to_, compl) ->
            let entries = compl.entries in
            Protocol.Completions { from; to_; entries }
        | None -> Protocol.Completions { from = 0; to_ = 0; entries = [] })
    | Type_enclosing (source, position) ->
        let source = Msource.make source in
        let query = Query_protocol.Type_enclosing (None, position, None) in
        Protocol.Typed_enclosings (dispatch source query)
    | Protocol.All_errors source ->
        let source = Msource.make source in
        let query =
          Query_protocol.Errors { lexing = true; parsing = true; typing = true }
        in
        let errors =
          dispatch source query
          |> List.map
               ~f:(fun (Location.{ kind; main = _; sub; source } as error) ->
                 let of_sub sub =
                   Location.print_sub_msg Format.str_formatter sub;
                   String.trim (Format.flush_str_formatter ())
                 in
                 let loc = Location.loc_of_report error in
                 let main =
                   Format.asprintf "@[%a@]" Location.print_main error
                   |> String.trim
                 in
                 Protocol.
                   { kind; loc; main; sub = List.map ~f:of_sub sub; source })
        in
        Protocol.Errors errors
    | Add_cmis cmis -> add_cmis cmis
  in
  let res = Marshal.to_bytes res [] in
  Js_of_ocaml.Worker.post_message res

let run () = Js_of_ocaml.Worker.set_onmessage on_message