this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #4 from jonludlam/dynamic-cmis

Dynamic cmis

authored by

Ulysse and committed by
GitHub
c7560fab 29a7f686

+331 -15
+10
README.md
··· 1 1 # merlin-js 2 2 3 3 Try-it: [https://voodoos.github.io/merlin-js](https://voodoos.github.io/merlin-js) 4 + 5 + To run locally, run 'make' then launch an http server from the 'examples' directory. For example: 6 + 7 + ```sh 8 + make 9 + cd examples 10 + python3 -m http.server 11 + ``` 12 + 13 +
+4 -1
example/app.ml
··· 1 1 open Code_mirror 2 2 3 3 module Merlin = 4 - Merlin_codemirror.Make (struct let worker_url = "merlin_worker.bc.js" end) 4 + Merlin_codemirror.Make (struct 5 + let worker_url = "merlin_worker.bc.js" 6 + let cmis = { Protocol.static_cmis = Static_files.stdlib_cmis; dynamic_cmis = None } 7 + end) 5 8 6 9 let basic_setup = Jv.get Jv.global "__CM__basic_setup" |> Extension.of_jv 7 10
+14 -1
example/dune
··· 2 2 (name app) 3 3 (modes js) 4 4 (promote) 5 + (modules App) 6 + (libraries 7 + merlin_client 8 + code-mirror 9 + merlin-js.code-mirror 10 + merlin-js.worker.static)) 11 + 12 + (executable 13 + (name dynamic) 14 + (modes js) 15 + (promote) 16 + (modules Dynamic) 5 17 (libraries 6 18 merlin_client 7 19 code-mirror ··· 15 27 (name all-js) 16 28 (deps 17 29 merlin_worker.bc.js 18 - app.bc.js)) 30 + app.bc.js 31 + dynamic.bc.js))
+89
example/dynamic.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="utf-8"> 6 + <meta name="viewport" content="width=device-width,initial-scale=1.0"> 7 + <script type="text/javascript" defer src="dynamic.bc.js"></script> 8 + <!-- <script type="text/javascript" defer src="index.js"></script> --> 9 + <title>Merlin the web</title> 10 + <style> 11 + @import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap'); 12 + 13 + body { 14 + background-color: #4c4956; 15 + font-family: 'Fira Code', monospace 16 + } 17 + 18 + p { 19 + width: 50%; 20 + margin: auto; 21 + margin-top: 2rem; 22 + color: white; 23 + } 24 + 25 + a { 26 + color: lightskyblue 27 + } 28 + 29 + #editor { 30 + min-width: 30rem; 31 + width: 50%; 32 + height: 500px; 33 + border: 1px solid #ddd; 34 + border-radius: 0.5rem; 35 + overflow: hidden; 36 + margin: auto; 37 + margin-top: 2rem; 38 + background-color: white; 39 + } 40 + 41 + @media (max-width: 1200px) { 42 + #editor { 43 + min-width: none; 44 + width: 90%; 45 + } 46 + } 47 + 48 + 49 + .cm-editor { 50 + height: 100%; 51 + } 52 + 53 + .cm-tooltip, 54 + .cm-completionLabel, 55 + .cm-completionMatchedText, 56 + .cm-completionDetail { 57 + font-family: 'Fira Code', monospace; 58 + font-size: 0.8rem; 59 + } 60 + 61 + .cm-editor .cm-tooltip { 62 + max-width: 25rem; 63 + border-radius: 0.25rem; 64 + overflow: hidden; 65 + } 66 + 67 + .cm-editor .cm-tooltip-hover { 68 + padding-left: 0.5rem; 69 + padding-right: 0.5rem; 70 + padding-top: 0.25rem; 71 + padding-bottom: 0.25rem; 72 + } 73 + 74 + .cm-editor .cm-content { 75 + font-family: 'Fira Code', monospace; 76 + } 77 + </style> 78 + </head> 79 + 80 + <body> 81 + <noscript>Sorry, you need to enable JavaScript to see this page.</noscript> 82 + <p>This example demonstrates merlin with dynamic loading of cmis. Load the 83 + 'Network' tab of the developer tools to see the requests and responses. 84 + See also the <a href="index.html">static example</a>. 85 + </p> 86 + <div id="editor"></div> 87 + </body> 88 + 89 + </html>
+44
example/dynamic.ml
··· 1 + open Code_mirror 2 + 3 + module Merlin = 4 + Merlin_codemirror.Make (struct 5 + let worker_url = "merlin_worker.bc.js" 6 + let cmis = 7 + let dcs_toplevel_modules = [ 8 + "CamlinternalAtomic"; 9 + "CamlinternalFormat"; 10 + "CamlinternalFormatBasics"; 11 + "CamlinternalLazy"; 12 + "CamlinternalMod"; 13 + "CamlinternalOO"; 14 + "Std_exit"; 15 + "Stdlib"; 16 + "Unix"; 17 + "UnixLabels"; 18 + ] in 19 + let dcs_url = "/static/stdlib/" in 20 + let dcs_file_prefixes = ["stdlib__"] in 21 + { Protocol.static_cmis = []; 22 + dynamic_cmis = Some { 23 + dcs_url; dcs_toplevel_modules; dcs_file_prefixes } } 24 + end) 25 + 26 + let basic_setup = Jv.get Jv.global "__CM__basic_setup" |> Extension.of_jv 27 + 28 + let init ?doc ?(exts = [||]) () = 29 + let open Editor in 30 + let extensions = 31 + Array.append [| basic_setup; Merlin_codemirror.ocaml |] exts 32 + in 33 + let config = 34 + State.Config.create ?doc ~extensions () 35 + in 36 + let state = State.create ~config () in 37 + let opts = View.opts 38 + ~state 39 + ~parent:(Merlin_codemirror.Utils.get_el_by_id "editor") () 40 + in 41 + let view : View.t = View.create ~opts () in 42 + (state, view) 43 + 44 + let _editor = init ~exts:Merlin.all_extensions ()
+13
example/index.html
··· 14 14 background-color: #4c4956; 15 15 font-family: 'Fira Code', monospace 16 16 } 17 + 18 + p { 19 + width: 50%; 20 + margin: auto; 21 + margin-top: 2rem; 22 + color: white; 23 + } 17 24 25 + a { 26 + color: lightskyblue 27 + } 28 + 18 29 #editor { 19 30 min-width: 30rem; 20 31 width: 50%; ··· 68 79 69 80 <body> 70 81 <noscript>Sorry, you need to enable JavaScript to see this page.</noscript> 82 + <p>This example demonstrates merlin with all cmis embedded. See <a href="dynamic.html">dynamic.html</a> 83 + for an example of dynamically loaded cmis.</p> 71 84 <div id="editor"></div> 72 85 </body> 73 86
+1
example/static
··· 1 + ../src/worker/static
+9
src/client/merlin_client.ml
··· 66 66 match data with 67 67 | Protocol.Typed_enclosings l -> l 68 68 | _ -> assert false 69 + 70 + let add_cmis worker cmis = 71 + let open Fut.Syntax in 72 + let action = Protocol.Add_cmis cmis in 73 + let+ data : Protocol.answer = query ~action worker in 74 + Console.(log ["Received response from adding cmis:"; data]); 75 + match data with 76 + | Protocol.Added_cmis -> () 77 + | _ -> assert false
+11 -2
src/extension/merlin_codemirror.ml
··· 79 79 let ocaml = Jv.get Jv.global "__CM__mllike" |> Stream.Language.of_jv 80 80 let ocaml = Stream.Language.define ocaml 81 81 82 - module Make (Config : sig val worker_url : string end) = struct 83 - let worker = Merlin_client.make_worker Config.worker_url 82 + module type Config = sig 83 + val worker_url : string 84 + val cmis : Protocol.cmis 85 + end 86 + 87 + module Make (Config : Config) = struct 88 + let worker = 89 + let worker = Merlin_client.make_worker Config.worker_url in 90 + let _ = Merlin_client.add_cmis worker Config.cmis in 91 + worker 92 + 84 93 let autocomplete = autocomplete worker 85 94 let tooltip_on_hover = tooltip_on_hover worker 86 95 let linter = Lint.create (linter worker)
+12 -1
src/extension/merlin_codemirror.mli
··· 6 6 val ocaml : Code_mirror.Extension.t 7 7 (** An extension providing OCaml syntax highlighting *) 8 8 9 - module Make : functor (Config : sig val worker_url : string end) -> sig 9 + module type Config = sig 10 + val worker_url : string 11 + (** The url of the worker javascript file *) 12 + 13 + val cmis : Protocol.cmis 14 + (** CMIs are required for merlin to work correctly. These can either be 15 + provided statically or provided as a list of URLs from which the 16 + CMIs can be downloaded. If using URLs, these will only be 17 + downloaded on demand. *) 18 + end 19 + 20 + module Make : functor (Config : Config) -> sig 10 21 val autocomplete : Code_mirror.Extension.t 11 22 (** An extension providing completions when typing *) 12 23
+39
src/protocol/protocol.ml
··· 3 3 4 4 type source = string 5 5 6 + (** CMIs are provided either statically or as URLs to be downloaded on demand *) 7 + 8 + (** Dynamic cmis are loaded from beneath the given url. In addition the 9 + top-level modules are specified, and prefixes for other modules. For 10 + example, for the OCaml standard library, a user might pass: 11 + 12 + {[ 13 + { dcs_url="/static/stdlib"; 14 + dcs_toplevel_modules=["Stdlib"]; 15 + dcs_file_prefixes=["stdlib__"]; } 16 + ]} 17 + 18 + In which case, merlin will expect to be able to download a valid file 19 + from the url ["/static/stdlib/stdlib.cmi"] corresponding to the 20 + specified toplevel module, and it will also attempt to download any 21 + module with the prefix ["Stdlib__"] from the same base url, so for 22 + example if an attempt is made to look up the module ["Stdlib__Foo"] 23 + then merlin-js will attempt to download a file from the url 24 + ["/static/stdlib/stdlib__Foo.cmi"]. 25 + *) 26 + 27 + type dynamic_cmis = { 28 + dcs_url : string; 29 + dcs_toplevel_modules : string list; 30 + dcs_file_prefixes : string list; 31 + } 32 + 33 + type static_cmi = { 34 + sc_name : string; (* capitalised, e.g. 'Stdlib' *) 35 + sc_content : string; 36 + } 37 + 38 + type cmis = { 39 + static_cmis : static_cmi list; 40 + dynamic_cmis : dynamic_cmis option; 41 + } 42 + 6 43 type action = 7 44 | Complete_prefix of source * Msource.position 8 45 | Type_enclosing of source * Msource.position 9 46 | All_errors of source 47 + | Add_cmis of cmis 10 48 11 49 type error = { 12 50 kind : Location.report_kind; ··· 31 69 | Completions of completions 32 70 | Typed_enclosings of 33 71 (Location.t * [ `Index of int | `String of string ] * is_tail_position) list 72 + | Added_cmis 34 73 35 74 let report_source_to_string = function 36 75 | Location.Lexer -> "lexer"
-1
src/worker/dune
··· 5 5 (javascript_files stubs.js)) 6 6 (preprocess (pps js_of_ocaml-ppx)) 7 7 (libraries 8 - static_files 9 8 protocol 10 9 merlin-lib.kernel 11 10 merlin-lib.utils
+1
src/worker/static/dune
··· 5 5 (public_name merlin-js.worker.static) 6 6 (modules static_files) 7 7 (preprocess (pps ppx_blob)) 8 + (libraries merlin-js.protocol) 8 9 (preprocessor_deps 9 10 (glob_files stdlib/*.cmi))) 10 11
+3 -2
src/worker/static/gen_static.ml
··· 15 15 let stdlib = Filename.concat cwd "stdlib" in 16 16 let out = open_out "static_files.ml" in 17 17 18 - Printf.fprintf out "let stdlib_cmis = ["; 18 + Printf.fprintf out "open Protocol\nlet stdlib_cmis = ["; 19 19 let dir = Unix.opendir stdlib in 20 20 iter_cmi ~f:(fun file -> 21 21 let fullpath = Filename.concat stdlib file in 22 - Printf.fprintf out "(%S,[%%blob %S]);" fullpath fullpath) dir; 22 + let module_name = Filename.basename file |> String.capitalize_ascii in 23 + Printf.fprintf out "{sc_name=%S; sc_content=[%%blob %S]};" module_name fullpath) dir; 23 24 Printf.fprintf out "]\n"; 24 25 25 26 close_out out
+81 -7
src/worker/worker.ml
··· 3 3 open Merlin_kernel 4 4 module Location = Ocaml_parsing.Location 5 5 6 + let sync_get url = 7 + let open Js_of_ocaml in 8 + let x = XmlHttpRequest.create () in 9 + x##.responseType := Js.string "arraybuffer"; 10 + x##_open (Js.string "GET") (Js.string url) Js._false; 11 + x##send Js.null; 12 + match x##.status with 13 + | 200 -> 14 + Js.Opt.case 15 + (File.CoerceTo.arrayBuffer x##.response) 16 + (fun () -> 17 + Firebug.console##log (Js.string "Failed to receive file"); 18 + None) 19 + (fun b -> Some (Typed_array.String.of_arrayBuffer b)) 20 + | _ -> None 21 + 22 + let filename_of_module unit_name = 23 + Printf.sprintf "%s.cmi" (String.uncapitalize_ascii unit_name) 24 + 25 + let reset_dirs () = 26 + Ocaml_utils.Directory_content_cache.clear (); 27 + let open Ocaml_utils.Load_path in 28 + let dirs = get_paths () in 29 + reset (); 30 + List.iter ~f:(fun p -> 31 + prepend_dir (Dir.create p)) dirs 32 + 33 + let add_dynamic_cmis dcs = 34 + let open Ocaml_typing.Persistent_env.Persistent_signature in 35 + let old_loader = !load in 36 + 37 + let fetch = 38 + (fun filename -> 39 + let open Option.Infix in 40 + let url = Filename.concat dcs.Protocol.dcs_url filename in 41 + sync_get url) 42 + in 43 + 44 + List.iter ~f:(fun name -> 45 + let filename = filename_of_module name in 46 + match fetch (filename_of_module name) with 47 + | Some content -> 48 + let name = Filename.(concat "/static/stdlib" filename) in 49 + Js_of_ocaml.Sys_js.create_file ~name ~content 50 + | None -> ()) dcs.dcs_toplevel_modules; 51 + 52 + let new_load ~unit_name = 53 + let filename = filename_of_module unit_name in 54 + let fs_name = Filename.(concat "/static/stdlib" filename) in 55 + (* Check if it's already been downloaded. This will be the 56 + case for all toplevel cmis. Also check whether we're supposed 57 + to handle this cmi *) 58 + if 59 + not (Sys.file_exists fs_name) && 60 + List.exists ~f:(fun prefix -> 61 + String.starts_with ~prefix filename) dcs.dcs_file_prefixes 62 + then begin 63 + match fetch filename with 64 + | Some x -> 65 + Js_of_ocaml.Sys_js.create_file ~name:fs_name ~content:x; 66 + (* At this point we need to tell merlin that the dir contents 67 + have changed *) 68 + reset_dirs () 69 + | None -> 70 + Printf.eprintf "Warning: Expected to find cmi at: %s\n%!" 71 + (Filename.concat dcs.Protocol.dcs_url filename) 72 + end; 73 + old_loader ~unit_name 74 + in 75 + load := new_load 76 + 77 + let add_cmis { Protocol.static_cmis; dynamic_cmis } = 78 + List.iter static_cmis ~f:(fun { Protocol.sc_name; sc_content } -> 79 + let filename = Printf.sprintf "%s.cmi" (String.uncapitalize_ascii sc_name) in 80 + let name = Filename.(concat "/static/stdlib" filename) in 81 + Js_of_ocaml.Sys_js.create_file ~name ~content:sc_content); 82 + Option.iter ~f:add_dynamic_cmis dynamic_cmis; 83 + Protocol.Added_cmis 84 + 6 85 let config = 7 86 let initial = Mconfig.initial in 8 87 { initial with ··· 174 253 }) 175 254 in 176 255 Protocol.Errors errors 256 + | Add_cmis cmis -> 257 + add_cmis cmis 177 258 in 178 259 let res = Marshal.to_bytes res [] in 179 260 Js_of_ocaml.Worker.post_message res 180 261 181 - 182 262 let run () = 183 - (* Load the CMIs into the pseudo file-system *) 184 - (* This add roughly 3mo to the final script. These could be loaded dynamically 185 - after the worker *) 186 - List.iter Static_files.stdlib_cmis ~f:(fun ( path, content) -> 187 - let name = Filename.(concat "/static/stdlib" (basename path)) in 188 - Js_of_ocaml.Sys_js.create_file ~name ~content); 189 263 Js_of_ocaml.Worker.set_onmessage on_message