this repo has no description
0
fork

Configure Feed

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

Squashed 'merlin-js/' content from commit 8e1ffab4f

git-subtree-dir: merlin-js
git-subtree-split: 8e1ffab4f87f129cefd194f56d043074752aeb30

+1128
+49
.github/workflows/js.yml
··· 1 + # This is a basic workflow to help you get started with Actions 2 + 3 + name: CI 4 + 5 + # Controls when the action will run. Triggers the workflow on push or pull request 6 + # events but only for the master branch 7 + on: 8 + push: 9 + branches: [ main ] 10 + pull_request: 11 + branches: [ main ] 12 + 13 + # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 + jobs: 15 + # This workflow contains a single job called "build" 16 + build: 17 + strategy: 18 + fail-fast: false 19 + matrix: 20 + os: 21 + - ubuntu-latest 22 + ocaml-compiler: 23 + - 5.2.x 24 + # The type of runner that the job will run on 25 + runs-on: ${{ matrix.os }} 26 + 27 + # Steps represent a sequence of tasks that will be executed as part of the job 28 + steps: 29 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 30 + - uses: actions/checkout@v4 31 + 32 + - name: Set up OCaml ${{ matrix.ocaml-compiler }} 33 + uses: ocaml/setup-ocaml@v3 34 + with: 35 + # Version of the OCaml compiler to initialise 36 + ocaml-compiler: ${{ matrix.ocaml-compiler }} 37 + 38 + - name: Install dependencies 39 + run: opam install . --deps-only --with-test 40 + 41 + - name: Build and test in release mode 42 + run: opam exec -- make js 43 + 44 + - name: Deploy 45 + if: ${{ github.event_name == 'push' }} 46 + uses: peaceiris/actions-gh-pages@v3 47 + with: 48 + github_token: ${{ secrets.GITHUB_TOKEN }} 49 + publish_dir: ./example
+25
.gitignore
··· 1 + _build 2 + _opam 3 + .merlin 4 + jbuild-workspace 5 + dune-workspace 6 + *.install 7 + *.tar.gz 8 + *.pyc 9 + *.cmly 10 + *.elc 11 + 12 + /ocamlmerlin 13 + /ocamlmerlin-server 14 + /ocamlmerlin-lsp 15 + /dot-merlin-reader 16 + 17 + # Ignore garbage files from editors 18 + *.un~ 19 + *.swp 20 + *.swo 21 + 22 + # merlin js specific 23 + *.bc* 24 + node_modules 25 + index.js
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2023 Ulysse Gérard 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+10
Makefile
··· 1 + js: 2 + dune build @all-js --profile=release --ignore-promoted-rules 3 + 4 + js-dev: 5 + dune build @all-js --watch --terminal-persistence=clear-on-rebuild 6 + 7 + jsoo-node: 8 + cd vendor/jsoo-code-mirror && npm install 9 + 10 + .PHONY: js-dev js
+13
README.md
··· 1 + # merlin-js 2 + 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 +
+3
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name merlin-js)
+27
example/app.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 = { Protocol.static_cmis = Static_files.stdlib_cmis; dynamic_cmis = None } 7 + end) 8 + 9 + let basic_setup = Jv.get Jv.global "__CM__basic_setup" |> Extension.of_jv 10 + 11 + let init ?doc ?(exts = [||]) () = 12 + let open Editor in 13 + let extensions = 14 + Array.append [| basic_setup; Merlin_codemirror.ocaml |] exts 15 + in 16 + let config = 17 + State.Config.create ?doc ~extensions () 18 + in 19 + let state = State.create ~config () in 20 + let opts = View.opts 21 + ~state 22 + ~parent:(Merlin_codemirror.Utils.get_el_by_id "editor") () 23 + in 24 + let view : View.t = View.create ~opts () in 25 + (state, view) 26 + 27 + let _editor = init ~exts:Merlin.all_extensions ()
+31
example/dune
··· 1 + (executable 2 + (name app) 3 + (modes js) 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) 17 + (libraries 18 + merlin_client 19 + code-mirror 20 + merlin-js.code-mirror)) 21 + 22 + (copy_files 23 + (mode promote) 24 + (files worker/merlin_worker.bc.js)) 25 + 26 + (alias 27 + (name all-js) 28 + (deps 29 + merlin_worker.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 = "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 ()
+87
example/index.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="app.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 all cmis embedded. See <a href="dynamic.html">dynamic.html</a> 83 + for an example of dynamically loaded cmis.</p> 84 + <div id="editor"></div> 85 + </body> 86 + 87 + </html>
+1
example/stdlib
··· 1 + ../src/worker/static/stdlib
+6
example/worker/dune
··· 1 + (executable 2 + (name merlin_worker) 3 + (modules merlin_worker) 4 + (promote (until-clean)) 5 + (modes js) 6 + (libraries worker))
+1
example/worker/merlin_worker.ml
··· 1 + let () = Worker.run ()
+29
merlin-js.opam
··· 1 + opam-version: "2.0" 2 + maintainer: "thevoodoos@gmail.com" 3 + authors: "Ulysse Gérard" 4 + homepage: "https://github.com/voodoos/merlin-js" 5 + bug-reports: "https://github.com/voodoos/merlin-js/issues" 6 + dev-repo: "git+https://github.com/voodoos/merlin-js.git" 7 + license: "MIT" 8 + build: [ 9 + ["dune" "subst"] {dev} 10 + ["dune" "build" "-p" name "-j" jobs] 11 + ] 12 + depends: [ 13 + "ocaml" {>= "5.2" & < "5.4"} 14 + "dune" {>= "3.0"} 15 + "merlin-lib" 16 + "yojson" {>= "1.6.0"} 17 + "js_of_ocaml" {>= "6.0.1"} 18 + "js_of_ocaml-ppx" {>= "6.0.1"} 19 + "brr" {>= "0.0.4"} 20 + "ppx_blob" {>= "0.7.2"} 21 + "code-mirror" 22 + ] 23 + pin-depends: [ 24 + ["code-mirror.dev" "git+https://github.com/patricoferris/jsoo-code-mirror#8fe48910e265ff87f9fc94ceb7b3d19fac102a96"] 25 + ] 26 + synopsis: 27 + "Editor helper, provides completion, typing and source browsing for the web" 28 + description: 29 + "Merlin is an assistant for editing OCaml code. It aims to provide the features available in modern browsers: error reporting, auto completion, source browsing and much more."
+4
src/client/dune
··· 1 + (library 2 + (name merlin_client) 3 + (public_name merlin-js.client) 4 + (libraries protocol brr))
+91
src/client/merlin_client.ml
··· 1 + module type WORKER = sig 2 + type t 3 + val post : t -> Protocol.action -> unit 4 + end 5 + 6 + module Make (Worker : WORKER) = struct 7 + (* When a query is sent to the Worker we keep the Future result in an indexed 8 + table so that the on_message function will be able to determine the Future when 9 + the answer is posted by the Worker. 10 + The Worker works synchronously so we expect answer to arrive in order. *) 11 + type worker = { 12 + worker: Worker.t; 13 + queue: (Protocol.answer -> unit) Queue.t 14 + } 15 + 16 + let add_fut worker res = Queue.add res worker.queue 17 + let res_fut worker v = (Queue.take worker.queue) v 18 + 19 + let on_message worker data = res_fut worker data 20 + 21 + let make_worker worker = 22 + let queue = Queue.create () in 23 + { worker; queue } 24 + 25 + (* todo share that with worker *) 26 + type action = Completion | Type_enclosing | Errors 27 + 28 + type errors = Protocol.error list 29 + 30 + let query ~action worker (*todo: other queries*) = 31 + let fut, set = Fut.create () in 32 + add_fut worker set; 33 + Worker.post worker.worker action; 34 + fut 35 + 36 + let query_errors ?filename worker (source : string) = 37 + let open Fut.Syntax in 38 + let action = Protocol.All_errors (source, filename) in 39 + let+ data : Protocol.answer = query ~action worker in 40 + match data with 41 + | Protocol.Errors errors -> errors 42 + | _ -> assert false 43 + 44 + let query_completions ?filename worker (source : string) position = 45 + let open Fut.Syntax in 46 + let action = Protocol.Complete_prefix (source, position, filename) in 47 + let+ data : Protocol.answer = query ~action worker in 48 + match data with 49 + | Protocol.Completions compl -> compl 50 + | _ -> assert false 51 + 52 + let query_type ?filename worker (source : string) position = 53 + let open Fut.Syntax in 54 + let action = Protocol.Type_enclosing (source, position, filename) in 55 + let+ data : Protocol.answer = query ~action worker in 56 + match data with 57 + | Protocol.Typed_enclosings l -> l 58 + | _ -> assert false 59 + 60 + let add_cmis worker cmis = 61 + let open Fut.Syntax in 62 + let action = Protocol.Add_cmis cmis in 63 + let+ data : Protocol.answer = query ~action worker in 64 + match data with 65 + | Protocol.Added_cmis -> () 66 + | _ -> assert false 67 + end 68 + 69 + module Webworker = struct 70 + include Brr_webworkers.Worker 71 + 72 + let post t action = 73 + let bytes = Marshal.to_bytes action [] in 74 + post t bytes 75 + end 76 + 77 + include Make (Webworker) 78 + 79 + let make_worker url = 80 + let worker = make_worker @@ Webworker.create @@ Jstr.of_string url in 81 + let on_message m = 82 + let m = Brr.Ev.as_type m in 83 + let data_marshaled : bytes = Brr_io.Message.Ev.data m in 84 + let data : Protocol.answer = Marshal.from_bytes data_marshaled 0 in 85 + on_message worker data 86 + in 87 + let _listen = 88 + Brr.Ev.listen Brr_io.Message.Ev.message on_message 89 + @@ Webworker.as_target worker.worker 90 + in 91 + worker
+11
src/extension/dune
··· 1 + (library 2 + (name merlin_codemirror) 3 + (public_name merlin-js.code-mirror) 4 + (libraries 5 + brr 6 + merlin_client 7 + code-mirror 8 + code-mirror.lint 9 + code-mirror.autocomplete 10 + code-mirror.tooltip 11 + code-mirror.stream))
+113
src/extension/merlin_codemirror.ml
··· 1 + open Code_mirror 2 + open Brr 3 + 4 + module Utils = Utils 5 + 6 + let ocaml = Jv.get Jv.global "__CM__mllike" |> Stream.Language.of_jv 7 + let ocaml = Stream.Language.define ocaml 8 + 9 + module Extensions (Worker : Merlin_client.WORKER) = struct 10 + 11 + module Merlin_client = Merlin_client.Make (Worker) 12 + type worker = Merlin_client.worker 13 + 14 + let linter worker = fun view -> 15 + let open Fut.Syntax in 16 + let doc = Utils.get_full_doc @@ Editor.View.state view in 17 + let+ result = Merlin_client.query_errors worker doc in 18 + List.map (fun Protocol.{ kind; loc; main; sub = _; source } -> 19 + let from = loc.loc_start.pos_cnum in 20 + let to_ = loc.loc_end.pos_cnum in 21 + let source = Protocol.report_source_to_string source in 22 + let severity = match kind with 23 + | Report_error 24 + | Report_warning_as_error _ 25 + | Report_alert_as_error _ -> Lint.Diagnostic.Error 26 + | Report_warning _ -> Lint.Diagnostic.Warning 27 + | Report_alert _ -> Lint.Diagnostic.Info 28 + in 29 + Lint.Diagnostic.create ~source ~from ~to_ ~severity ~message:main () 30 + ) result 31 + |> Array.of_list 32 + 33 + let keywords = List.map 34 + (fun label -> 35 + Autocomplete.Completion.create ~label ~type_:"keyword" ()) 36 + [ 37 + "as"; "do"; "else"; "end"; "exception"; "fun"; "functor"; "if"; "in"; 38 + "include"; "let"; "of"; "open"; "rec"; "struct"; "then"; "type"; "val"; 39 + "while"; "with"; "and"; "assert"; "begin"; "class"; "constraint"; 40 + "done"; "downto"; "external"; "function"; "initializer"; "lazy"; 41 + "match"; "method"; "module"; "mutable"; "new"; "nonrec"; "object"; 42 + "private"; "sig"; "to"; "try"; "value"; "virtual"; "when"; 43 + ] 44 + 45 + let merlin_completion worker = fun ctx -> 46 + let open Fut.Syntax in 47 + let source = Utils.get_full_doc @@ Autocomplete.Context.state ctx in 48 + let pos = Autocomplete.Context.pos ctx in 49 + let+ { from; to_; entries } = 50 + Merlin_client.query_completions worker source (`Offset pos) 51 + in 52 + let options = 53 + let num_completions = List.length entries in 54 + List.mapi (fun i Query_protocol.Compl.{ name; desc; _ } -> 55 + let boost = num_completions - i in 56 + Autocomplete.Completion.create ~label:name ~detail:desc ~boost ()) entries 57 + in 58 + Some (Autocomplete.Result.create ~filter:true ~from ~to_ ~options ()) 59 + 60 + let autocomplete worker = 61 + let override = [ 62 + Autocomplete.Source.from_list keywords; 63 + Autocomplete.Source.create @@ merlin_completion worker] 64 + in 65 + let config = Autocomplete.config () ~override in 66 + Autocomplete.create ~config () 67 + 68 + let tooltip_on_hover worker = 69 + let open Tooltip in 70 + hover_tooltip @@ 71 + fun ~view ~pos ~side:_ -> 72 + let open Fut.Syntax in 73 + let doc = Utils.get_full_doc @@ Editor.View.state view in 74 + let pos = `Offset pos in 75 + let+ result = Merlin_client.query_type worker doc pos in 76 + match result with 77 + | (loc, `String type_, _)::_ -> 78 + let create _view = 79 + let dom = El.div [El.txt' type_] in 80 + Tooltip_view.create ~dom () 81 + in 82 + let pos = loc.loc_start.pos_cnum in 83 + let end_ = loc.loc_end.pos_cnum in 84 + Some (Tooltip.create ~pos ~end_ ~above:true ~arrow:true ~create ()) 85 + | _ -> None 86 + 87 + let linter worker = Lint.create (linter worker) 88 + 89 + let all_extensions worker = [| 90 + linter worker; 91 + autocomplete worker; 92 + tooltip_on_hover worker 93 + |] 94 + end 95 + 96 + module type Config = sig 97 + val worker_url : string 98 + val cmis : Protocol.cmis 99 + end 100 + 101 + module Make (Config : Config) = struct 102 + let worker = 103 + let worker = Merlin_client.make_worker Config.worker_url in 104 + let _ = Merlin_client.add_cmis worker Config.cmis in 105 + worker 106 + 107 + open Extensions (Merlin_client.Webworker) 108 + 109 + let autocomplete = autocomplete worker 110 + let tooltip_on_hover = tooltip_on_hover worker 111 + let linter = linter worker 112 + let all_extensions = all_extensions worker 113 + end
+52
src/extension/merlin_codemirror.mli
··· 1 + module Utils : sig 2 + val get_el_by_id : string -> Brr.El.t 3 + val get_full_doc : Code_mirror.Editor.State.t -> string 4 + end 5 + 6 + val ocaml : Code_mirror.Extension.t 7 + (** An extension providing OCaml syntax highlighting *) 8 + 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 21 + 22 + val autocomplete : Code_mirror.Extension.t 23 + (** An extension providing completions when typing *) 24 + 25 + val tooltip_on_hover : Code_mirror.Extension.t 26 + (** An extension providing type-information when hovering code *) 27 + 28 + val linter : Code_mirror.Extension.t 29 + (** An extension that highlights errors and warnings in the code *) 30 + 31 + val all_extensions : Code_mirror.Extension.t array 32 + (** All the Merlin-specific extensions (does not include [ocaml]) *) 33 + 34 + end 35 + 36 + module Extensions (Worker : Merlin_client.WORKER) : sig 37 + 38 + type worker = Merlin_client.Make(Worker).worker 39 + 40 + val autocomplete : worker -> Code_mirror.Extension.t 41 + (** An extension providing completions when typing *) 42 + 43 + val tooltip_on_hover : worker -> Code_mirror.Extension.t 44 + (** An extension providing type-information when hovering code *) 45 + 46 + val linter : worker -> Code_mirror.Extension.t 47 + (** An extension that highlights errors and warnings in the code *) 48 + 49 + val all_extensions : worker -> Code_mirror.Extension.t array 50 + (** All the Merlin-specific extensions (does not include [ocaml]) *) 51 + 52 + end
+9
src/extension/utils.ml
··· 1 + open Code_mirror 2 + open Brr 3 + let get_el_by_id i = 4 + Brr.Document.find_el_by_id G.document (Jstr.of_string i) |> Option.get 5 + 6 + 7 + let get_full_doc state = 8 + let lines = Editor.(state |> State.doc |> Text.to_jstr_array) in 9 + lines |> Array.map Jstr.to_string |> Array.to_list |> String.concat "\n"
+7
src/protocol/dune
··· 1 + (library 2 + (name protocol) 3 + (public_name merlin-js.protocol) 4 + (libraries 5 + merlin-lib.ocaml_parsing 6 + merlin-lib.query_protocol 7 + merlin-lib.kernel))
+81
src/protocol/protocol.ml
··· 1 + open Merlin_kernel 2 + module Location = Ocaml_parsing.Location 3 + 4 + type source = string 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 + 43 + type action = 44 + | Complete_prefix of source * Msource.position * string option 45 + | Type_enclosing of source * Msource.position * string option 46 + | All_errors of source * string option 47 + | Add_cmis of cmis 48 + 49 + type error = { 50 + kind : Location.report_kind; 51 + loc: Location.t; 52 + main : string; 53 + sub : string list; 54 + source : Location.error_source; 55 + } 56 + 57 + type completions = { 58 + from: int; 59 + to_: int; 60 + entries : Query_protocol.Compl.entry list 61 + } 62 + 63 + type is_tail_position = 64 + [`No | `Tail_position | `Tail_call] 65 + 66 + (* type errors = { from: int; to_: int; entries: error list } *) 67 + type answer = 68 + | Errors of error list 69 + | Completions of completions 70 + | Typed_enclosings of 71 + (Location.t * [ `Index of int | `String of string ] * is_tail_position) list 72 + | Added_cmis 73 + 74 + let report_source_to_string = function 75 + | Location.Lexer -> "lexer" 76 + | Location.Parser -> "parser" 77 + | Location.Typer -> "typer" 78 + | Location.Warning -> "warning" (* todo incorrect ?*) 79 + | Location.Unknown -> "unknown" 80 + | Location.Env -> "env" 81 + | Location.Config -> "config"
+14
src/worker/dune
··· 1 + (library 2 + (name worker) 3 + (public_name merlin-js.worker) 4 + (js_of_ocaml 5 + (javascript_files stubs.js)) 6 + (preprocess (pps js_of_ocaml-ppx)) 7 + (libraries 8 + protocol 9 + merlin-lib.kernel 10 + merlin-lib.utils 11 + merlin-lib.query_protocol 12 + merlin-lib.query_commands 13 + merlin-lib.ocaml_parsing 14 + js_of_ocaml))
+15
src/worker/static/dune
··· 1 + (data_only_dirs stdlib) 2 + 3 + (library 4 + (name static_files) 5 + (public_name merlin-js.worker.static) 6 + (modules static_files) 7 + (preprocess (pps ppx_blob)) 8 + (libraries merlin-js.protocol) 9 + (preprocessor_deps 10 + (glob_files stdlib/*.cmi))) 11 + 12 + (rule 13 + (target static_files.ml) 14 + (deps (glob_files stdlib/*.cmi)) 15 + (action (run ocaml %{dep:gen_static.ml})))
+26
src/worker/static/gen_static.ml
··· 1 + #use "topfind" ;; 2 + #require "unix";; 3 + 4 + 5 + let rec iter_cmi ~f dir_handle = 6 + match Unix.readdir dir_handle with 7 + | exception End_of_file -> () 8 + | file -> 9 + if Filename.extension file = ".cmi" then 10 + f file; 11 + iter_cmi ~f dir_handle 12 + 13 + let () = 14 + let cwd = Unix.getcwd () in 15 + let stdlib = Filename.concat cwd "stdlib" in 16 + let out = open_out "static_files.ml" in 17 + 18 + Printf.fprintf out "open Protocol\nlet stdlib_cmis = ["; 19 + let dir = Unix.opendir stdlib in 20 + iter_cmi ~f:(fun file -> 21 + let fullpath = Filename.concat stdlib file in 22 + let module_name = Filename.basename file |> String.capitalize_ascii |> Filename.remove_extension in 23 + Printf.fprintf out "{sc_name=%S; sc_content=[%%blob %S]};" module_name fullpath) dir; 24 + Printf.fprintf out "]\n"; 25 + 26 + close_out out
src/worker/static/stdlib/arith_status.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/big_int.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/bigarray.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalAtomic.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalFormat.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalFormatBasics.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalLazy.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalMod.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/camlinternalOO.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/dynlink.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/event.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/profiling.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/ratio.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/runtime_events.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/std_exit.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Arg.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Array.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__ArrayLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Atomic.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Bigarray.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Bool.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Buffer.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Bytes.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__BytesLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Callback.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Char.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Complex.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Condition.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Digest.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Domain.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Dynarray.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Effect.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Either.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Ephemeron.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Filename.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Float.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Format.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Fun.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Gc.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Genlex.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Hashtbl.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__In_channel.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Int.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Int32.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Int64.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Lazy.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Lexing.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__List.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__ListLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Map.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Marshal.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__MoreLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Mutex.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Nativeint.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Obj.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Oo.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Option.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Out_channel.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Parsing.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Pervasives.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Printexc.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Printf.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Queue.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Random.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Result.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Scanf.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Semaphore.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Seq.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Set.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Stack.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__StdLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Stream.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__String.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__StringLabels.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Sys.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Type.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Uchar.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Unit.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/stdlib__Weak.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/str.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/thread.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/topdirs.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/unix.cmi

This is a binary file and will not be displayed.

src/worker/static/stdlib/unixLabels.cmi

This is a binary file and will not be displayed.

+4
src/worker/stubs.js
··· 1 + //Provides: caml_unix_times 2 + function caml_unix_times() { 3 + return 4.2 4 + }
+263
src/worker/worker.ml
··· 1 + open Merlin_utils 2 + open Std 3 + open Merlin_kernel 4 + module Location = Ocaml_parsing.Location 5 + 6 + let stdlib_path = "/static/cmis" 7 + 8 + let sync_get url = 9 + let open Js_of_ocaml in 10 + let x = XmlHttpRequest.create () in 11 + x##.responseType := Js.string "arraybuffer"; 12 + x##_open (Js.string "GET") (Js.string url) Js._false; 13 + x##send Js.null; 14 + match x##.status with 15 + | 200 -> 16 + Js.Opt.case 17 + (File.CoerceTo.arrayBuffer x##.response) 18 + (fun () -> 19 + Js_of_ocaml.Console.console##log (Js.string "Failed to receive file"); 20 + None) 21 + (fun b -> Some (Typed_array.String.of_arrayBuffer b)) 22 + | _ -> None 23 + 24 + let filename_of_module unit_name = 25 + Printf.sprintf "%s.cmi" (String.uncapitalize_ascii unit_name) 26 + 27 + let reset_dirs () = 28 + Ocaml_utils.Directory_content_cache.clear (); 29 + let open Ocaml_utils.Load_path in 30 + let { visible; hidden } = get_paths () in 31 + reset (); 32 + init ~auto_include:no_auto_include ~visible ~hidden 33 + 34 + let add_dynamic_cmis dcs = 35 + let open Ocaml_typing.Persistent_env.Persistent_signature in 36 + let old_loader = !load in 37 + 38 + let fetch = 39 + (fun filename -> 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 stdlib_path filename) in 49 + Js_of_ocaml.Sys_js.create_file ~name ~content 50 + | None -> ()) dcs.dcs_toplevel_modules; 51 + 52 + let new_load ~allow_hidden ~unit_name = 53 + let filename = filename_of_module unit_name in 54 + let fs_name = Filename.(concat stdlib_path 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 ~allow_hidden ~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 stdlib_path 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 + 85 + let config ?filename () = 86 + let initial = Mconfig.initial in 87 + let query = match filename with 88 + | Some f -> { initial.query with filename = f } 89 + | None -> initial.query 90 + in 91 + { initial with 92 + merlin = { initial.merlin with stdlib = Some stdlib_path }; 93 + query } 94 + 95 + let make_pipeline ?filename source = 96 + Mpipeline.make (config ?filename ()) source 97 + 98 + let dispatch ?filename source query = 99 + let pipeline = make_pipeline ?filename source in 100 + Mpipeline.with_pipeline pipeline @@ fun () -> ( 101 + Query_commands.dispatch pipeline query 102 + ) 103 + 104 + module Completion = struct 105 + (* Prefixing code from ocaml-lsp-server *) 106 + let rfindi = 107 + let rec loop s ~f i = 108 + if i < 0 then 109 + None 110 + else if f (String.unsafe_get s i) then 111 + Some i 112 + else 113 + loop s ~f (i - 1) 114 + in 115 + fun ?from s ~f -> 116 + let from = 117 + let len = String.length s in 118 + match from with 119 + | None -> len - 1 120 + | Some i -> 121 + if i > len - 1 then 122 + raise @@ Invalid_argument "rfindi: invalid from" 123 + else 124 + i 125 + in 126 + loop s ~f from 127 + let lsplit2 s ~on = 128 + match String.index_opt s on with 129 + | None -> None 130 + | Some i -> 131 + let open String in 132 + Some (sub s ~pos:0 ~len:i, sub s ~pos:(i + 1) ~len:(length s - i - 1)) 133 + 134 + (** @see <https://ocaml.org/manual/lex.html> reference *) 135 + let prefix_of_position ?(short_path = false) source position = 136 + match Msource.text source with 137 + | "" -> "" 138 + | text -> 139 + let from = 140 + let (`Offset index) = Msource.get_offset source position in 141 + min (String.length text - 1) (index - 1) 142 + in 143 + let pos = 144 + let should_terminate = ref false in 145 + let has_seen_dot = ref false in 146 + let is_prefix_char c = 147 + if !should_terminate then 148 + false 149 + else 150 + match c with 151 + | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '\'' | '_' 152 + (* Infix function characters *) 153 + | '$' | '&' | '*' | '+' | '-' | '/' | '=' | '>' 154 + | '@' | '^' | '!' | '?' | '%' | '<' | ':' | '~' | '#' -> 155 + true 156 + | '`' -> 157 + if !has_seen_dot then 158 + false 159 + else ( 160 + should_terminate := true; 161 + true 162 + ) | '.' -> 163 + has_seen_dot := true; 164 + not short_path 165 + | _ -> false 166 + in 167 + rfindi text ~from ~f:(fun c -> not (is_prefix_char c)) 168 + in 169 + let pos = 170 + match pos with 171 + | None -> 0 172 + | Some pos -> pos + 1 173 + in 174 + let len = from - pos + 1 in 175 + let reconstructed_prefix = String.sub text ~pos ~len in 176 + (* if we reconstructed [~f:ignore] or [?f:ignore], we should take only 177 + [ignore], so: *) 178 + if 179 + String.is_prefixed ~by:"~" reconstructed_prefix 180 + || String.is_prefixed ~by:"?" reconstructed_prefix 181 + then 182 + match lsplit2 reconstructed_prefix ~on:':' with 183 + | Some (_, s) -> s 184 + | None -> reconstructed_prefix 185 + else 186 + reconstructed_prefix 187 + 188 + 189 + let at_pos ?filename source position = 190 + let prefix = prefix_of_position source position in 191 + let `Offset to_ = Msource.get_offset source position in 192 + let from = 193 + to_ - String.length (prefix_of_position ~short_path:true source position) 194 + in 195 + if prefix = "" then 196 + None 197 + else 198 + let query = Query_protocol.Complete_prefix (prefix, position, [], true, true) 199 + in 200 + Some (from, to_, dispatch ?filename source query) 201 + end 202 + (* 203 + let dump () = 204 + let query = Query_protocol.Dump [`String "paths"] in 205 + dispatch (Msource.make "") query *) 206 + 207 + (* let dump_config () = 208 + let pipeline = make_pipeline (Msource.make "") in 209 + Mpipeline.with_pipeline pipeline @@ fun () -> 210 + Mconfig.dump (Mpipeline.final_config pipeline) 211 + |> Json.pretty_to_string *) 212 + 213 + let on_message = function 214 + | Protocol.Complete_prefix (source, position, filename) -> 215 + let source = Msource.make source in 216 + begin match Completion.at_pos ?filename source position with 217 + | Some (from, to_, compl) -> 218 + let entries = compl.entries in 219 + Protocol.Completions { from; to_; entries; } 220 + | None -> 221 + Protocol.Completions { from = 0; to_ = 0; entries = []; } 222 + end 223 + | Type_enclosing (source, position, filename) -> 224 + let source = Msource.make source in 225 + let query = Query_protocol.Type_enclosing (None, position, None) in 226 + Protocol.Typed_enclosings (dispatch ?filename source query) 227 + | Protocol.All_errors (source, filename) -> 228 + let source = Msource.make source in 229 + let query = Query_protocol.Errors { 230 + lexing = true; 231 + parsing = true; 232 + typing = true; 233 + } 234 + in 235 + let errors = 236 + dispatch ?filename source query 237 + |> List.map ~f:(fun (Location.{kind; sub; source; _} as error) -> 238 + let of_sub sub = 239 + Location.print_sub_msg Format.str_formatter sub; 240 + String.trim (Format.flush_str_formatter ()) 241 + in 242 + let loc = Location.loc_of_report error in 243 + let main = 244 + Format.asprintf "@[%a@]" Location.print_main error |> String.trim 245 + in 246 + Protocol.{ 247 + kind; 248 + loc; 249 + main; 250 + sub = List.map ~f:of_sub sub; 251 + source; 252 + }) 253 + in 254 + Protocol.Errors errors 255 + | Add_cmis cmis -> 256 + add_cmis cmis 257 + 258 + let run () = 259 + Js_of_ocaml.Worker.set_onmessage @@ fun marshaled_message -> 260 + let action : Protocol.action = Marshal.from_bytes marshaled_message 0 in 261 + let res = on_message action in 262 + let res = Marshal.to_bytes res [] in 263 + Js_of_ocaml.Worker.post_message res
+2
src/worker/worker.mli
··· 1 + val on_message : Protocol.action -> Protocol.answer 2 + val run : unit -> unit