├── example ├── stdlib │ ├── .gitignore │ └── dune ├── worker │ ├── merlin_worker.ml │ └── dune ├── dune ├── app.ml ├── dynamic.ml ├── index.html └── dynamic.html ├── dune-project ├── src ├── worker │ ├── worker.mli │ ├── dune │ ├── static │ │ ├── dune │ │ └── gen_static.ml │ └── worker.ml ├── client │ ├── dune │ └── merlin_client.ml ├── protocol │ ├── dune │ └── protocol.ml └── extension │ ├── dune │ ├── utils.ml │ ├── merlin_codemirror.mli │ └── merlin_codemirror.ml ├── Makefile ├── README.md ├── .gitignore ├── merlin-js.opam ├── LICENSE └── .github └── workflows └── js.yml /example/stdlib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !dune 4 | -------------------------------------------------------------------------------- /example/worker/merlin_worker.ml: -------------------------------------------------------------------------------- 1 | let () = Worker.run () 2 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.20) 2 | 3 | (name merlin-js) 4 | -------------------------------------------------------------------------------- /src/worker/worker.mli: -------------------------------------------------------------------------------- 1 | val on_message : Protocol.action -> Protocol.answer 2 | val run : unit -> unit 3 | -------------------------------------------------------------------------------- /src/client/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name merlin_client) 3 | (public_name merlin-js.client) 4 | (libraries js_of_ocaml protocol brr)) 5 | -------------------------------------------------------------------------------- /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)) 8 | -------------------------------------------------------------------------------- /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 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)) 12 | -------------------------------------------------------------------------------- /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 | 14 | -------------------------------------------------------------------------------- /src/worker/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name worker) 3 | (public_name merlin-js.worker) 4 | (preprocess (pps js_of_ocaml-ppx)) 5 | (libraries 6 | protocol 7 | merlin-lib.kernel 8 | merlin-lib.utils 9 | merlin-lib.query_protocol 10 | merlin-lib.query_commands 11 | merlin-lib.ocaml_parsing 12 | js_of_ocaml)) 13 | -------------------------------------------------------------------------------- /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" 10 | -------------------------------------------------------------------------------- /example/worker/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name merlin_worker) 3 | (modules merlin_worker) 4 | (promote (until-clean)) 5 | (modes js wasm) 6 | (preprocess (pps js_of_ocaml-ppx)) 7 | (libraries worker js_of_ocaml)) 8 | 9 | 10 | (alias 11 | (name all-js) 12 | (deps 13 | merlin_worker.bc.js 14 | merlin_worker.bc.wasm.js 15 | app.bc.js 16 | dynamic.bc.js)) 17 | -------------------------------------------------------------------------------- /.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 26 | -------------------------------------------------------------------------------- /example/stdlib/dune: -------------------------------------------------------------------------------- 1 | (copy_files 2 | (alias all-js) 3 | (mode (promote (until-clean))) 4 | (files %{ocaml-config:standard_library}/*.cmi)) 5 | 6 | (copy_files 7 | (alias all-js) 8 | (mode (promote (until-clean))) 9 | (files %{ocaml-config:standard_library}/unix/*.cmi)) 10 | 11 | (copy_files 12 | (alias all-js) 13 | (mode (promote (until-clean))) 14 | (files %{ocaml-config:standard_library}/str/*.cmi)) 15 | -------------------------------------------------------------------------------- /example/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name app) 3 | (modes js) 4 | (promote (until-clean)) 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 (until-clean)) 16 | (modules Dynamic) 17 | (libraries 18 | merlin_client 19 | code-mirror 20 | merlin-js.code-mirror)) 21 | 22 | (alias 23 | (name all-js) 24 | (deps 25 | app.bc.js 26 | dynamic.bc.js)) 27 | -------------------------------------------------------------------------------- /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 15 | (glob_files %{ocaml-config:standard_library}/*.cmi) 16 | (glob_files %{ocaml-config:standard_library}/unix/*.cmi) 17 | (glob_files %{ocaml-config:standard_library}/str/*.cmi)) 18 | (action (run ocaml %{dep:gen_static.ml} %{deps}))) 19 | 20 | -------------------------------------------------------------------------------- /src/worker/static/gen_static.ml: -------------------------------------------------------------------------------- 1 | #use "topfind" ;; 2 | #require "unix";; 3 | 4 | let () = 5 | let out = open_out "static_files.ml" in 6 | 7 | Printf.fprintf out "open Protocol\nlet stdlib_cmis = ["; 8 | for i = 1 to Array.length Sys.argv - 1 do 9 | let fullpath = Sys.argv.(i) in 10 | let module_name = 11 | Filename.basename fullpath 12 | |> String.capitalize_ascii 13 | |> Filename.remove_extension 14 | in 15 | Printf.fprintf out "{sc_name=%S; sc_content=[%%blob %S]};" module_name fullpath 16 | done; 17 | Printf.fprintf out "]\n"; 18 | 19 | close_out out 20 | -------------------------------------------------------------------------------- /example/app.ml: -------------------------------------------------------------------------------- 1 | open Code_mirror 2 | 3 | module Merlin = 4 | Merlin_codemirror.Make (struct 5 | let worker_url = "worker/merlin_worker.bc.wasm.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 () 28 | -------------------------------------------------------------------------------- /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." 30 | -------------------------------------------------------------------------------- /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. 22 | -------------------------------------------------------------------------------- /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 () 45 | -------------------------------------------------------------------------------- /.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 50 | -------------------------------------------------------------------------------- /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) -> 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 Fut.t -> Code_mirror.Extension.t 41 | (** An extension providing completions when typing *) 42 | 43 | val tooltip_on_hover : worker Fut.t -> Code_mirror.Extension.t 44 | (** An extension providing type-information when hovering code *) 45 | 46 | val linter : worker Fut.t -> Code_mirror.Extension.t 47 | (** An extension that highlights errors and warnings in the code *) 48 | 49 | val all_extensions : worker Fut.t -> Code_mirror.Extension.t array 50 | (** All the Merlin-specific extensions (does not include [ocaml]) *) 51 | 52 | end 53 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 |This example demonstrates merlin with all cmis embedded. See dynamic.html 83 | for an example of dynamically loaded cmis.
84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/dynamic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |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 static example. 85 |
86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /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 45 | | Type_enclosing of source * Msource.position 46 | | All_errors of source 47 | | Add_cmis of cmis 48 | 49 | let action_to_string = function 50 | | Complete_prefix _ -> "Complete_prefix" 51 | | Type_enclosing _ -> "Type_enclosing" 52 | | All_errors _ -> "All_errors" 53 | | Add_cmis _ -> "Add_cmis" 54 | 55 | type error = { 56 | kind : Location.report_kind; 57 | loc: Location.t; 58 | main : string; 59 | sub : string list; 60 | source : Location.error_source; 61 | } 62 | 63 | type completions = { 64 | from: int; 65 | to_: int; 66 | entries : Query_protocol.Compl.entry list 67 | } 68 | 69 | type is_tail_position = 70 | [`No | `Tail_position | `Tail_call] 71 | 72 | (* type errors = { from: int; to_: int; entries: error list } *) 73 | type answer = 74 | | Ready 75 | | Errors of error list 76 | | Completions of completions 77 | | Typed_enclosings of 78 | (Location.t * [ `Index of int | `String of string ] * is_tail_position) list 79 | | Added_cmis 80 | 81 | let answer_to_string = function 82 | | Ready -> "Ready" 83 | | Errors _ -> "Errors" 84 | | Completions _ -> "Completions" 85 | | Typed_enclosings _ -> "Type_enclosings" 86 | | Added_cmis -> "Added_cmis" 87 | 88 | let report_source_to_string = function 89 | | Location.Lexer -> "lexer" 90 | | Location.Parser -> "parser" 91 | | Location.Typer -> "typer" 92 | | Location.Warning -> "warning" (* todo incorrect ?*) 93 | | Location.Unknown -> "unknown" 94 | | Location.Env -> "env" 95 | | Location.Config -> "config" 96 | -------------------------------------------------------------------------------- /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 worker (source : string) = 37 | let open Fut.Syntax in 38 | let action = Protocol.All_errors source 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 worker (source : string) position = 45 | let open Fut.Syntax in 46 | let action = Protocol.Complete_prefix (source, position) 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 worker (source : string) position = 53 | let open Fut.Syntax in 54 | let action = Protocol.Type_enclosing (source, position) 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_string action [] 74 | |> Js_of_ocaml.Js.bytestring in 75 | post t bytes 76 | end 77 | 78 | include Make (Webworker) 79 | 80 | let make_worker url = 81 | let worker = make_worker @@ Webworker.create @@ Jstr.of_string url in 82 | let ready, set_ready = Fut.create () in 83 | let on_message m = 84 | let m = Brr.Ev.as_type m in 85 | let data_marshaled = Brr_io.Message.Ev.data m |> Js_of_ocaml.Js.to_bytestring in 86 | let data : Protocol.answer = Marshal.from_string data_marshaled 0 in 87 | match data with 88 | | Protocol.Ready -> set_ready () 89 | | _ -> on_message worker data 90 | in 91 | let _listen = 92 | Brr.Ev.listen Brr_io.Message.Ev.message on_message 93 | @@ Webworker.as_target worker.worker 94 | in 95 | worker, ready 96 | -------------------------------------------------------------------------------- /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* worker = worker in 18 | let+ result = Merlin_client.query_errors worker doc in 19 | List.map (fun Protocol.{ kind; loc; main; sub = _; source } -> 20 | let from = loc.loc_start.pos_cnum in 21 | let to_ = loc.loc_end.pos_cnum in 22 | let source = Protocol.report_source_to_string source in 23 | let severity = match kind with 24 | | Report_error 25 | | Report_warning_as_error _ 26 | | Report_alert_as_error _ -> Lint.Diagnostic.Error 27 | | Report_warning _ -> Lint.Diagnostic.Warning 28 | | Report_alert _ -> Lint.Diagnostic.Info 29 | in 30 | Lint.Diagnostic.create ~source ~from ~to_ ~severity ~message:main () 31 | ) result 32 | |> Array.of_list 33 | 34 | let keywords = List.map 35 | (fun label -> 36 | Autocomplete.Completion.create ~label ~type_:"keyword" ()) 37 | [ 38 | "as"; "do"; "else"; "end"; "exception"; "fun"; "functor"; "if"; "in"; 39 | "include"; "let"; "of"; "open"; "rec"; "struct"; "then"; "type"; "val"; 40 | "while"; "with"; "and"; "assert"; "begin"; "class"; "constraint"; 41 | "done"; "downto"; "external"; "function"; "initializer"; "lazy"; 42 | "match"; "method"; "module"; "mutable"; "new"; "nonrec"; "object"; 43 | "private"; "sig"; "to"; "try"; "value"; "virtual"; "when"; 44 | ] 45 | 46 | let merlin_completion worker = fun ctx -> 47 | let open Fut.Syntax in 48 | let source = Utils.get_full_doc @@ Autocomplete.Context.state ctx in 49 | let pos = Autocomplete.Context.pos ctx in 50 | let+ { from; to_; entries } = 51 | let* worker = worker in 52 | Merlin_client.query_completions worker source (`Offset pos) 53 | in 54 | let options = 55 | let num_completions = List.length entries in 56 | List.mapi (fun i Query_protocol.Compl.{ name; desc; _ } -> 57 | let boost = num_completions - i in 58 | Autocomplete.Completion.create ~label:name ~detail:desc ~boost ()) entries 59 | in 60 | Some (Autocomplete.Result.create ~filter:true ~from ~to_ ~options ()) 61 | 62 | let autocomplete worker = 63 | let override = [ 64 | Autocomplete.Source.from_list keywords; 65 | Autocomplete.Source.create @@ merlin_completion worker] 66 | in 67 | let config = Autocomplete.config () ~override in 68 | Autocomplete.create ~config () 69 | 70 | let tooltip_on_hover worker = 71 | let open Tooltip in 72 | hover_tooltip @@ 73 | fun ~view ~pos ~side:_ -> 74 | let open Fut.Syntax in 75 | let doc = Utils.get_full_doc @@ Editor.View.state view in 76 | let pos = `Offset pos in 77 | let* worker = worker in 78 | let+ result = Merlin_client.query_type worker doc pos in 79 | match result with 80 | | (loc, `String type_, _)::_ -> 81 | let create _view = 82 | let dom = El.div [El.txt' type_] in 83 | Tooltip_view.create ~dom () 84 | in 85 | let pos = loc.loc_start.pos_cnum in 86 | let end_ = loc.loc_end.pos_cnum in 87 | Some (Tooltip.create ~pos ~end_ ~above:true ~arrow:true ~create ()) 88 | | _ -> None 89 | 90 | let linter worker = Lint.create (linter worker) 91 | 92 | let all_extensions worker = [| 93 | linter worker; 94 | autocomplete worker; 95 | tooltip_on_hover worker 96 | |] 97 | end 98 | 99 | module type Config = sig 100 | val worker_url : string 101 | val cmis : Protocol.cmis 102 | end 103 | 104 | module Make (Config : Config) = struct 105 | let worker = 106 | let open Fut.Syntax in 107 | let worker, ready = Merlin_client.make_worker Config.worker_url in 108 | let* () = ready in 109 | (* Initial Cmi loading should be the first request. Todo: make it clearer in 110 | the protocol that this is a mandatory initial exchange: Start worker -> 111 | worker sends Ready -> client sends Add_cmis *) 112 | let+ () = Merlin_client.add_cmis worker Config.cmis in 113 | worker 114 | 115 | open Extensions (Merlin_client.Webworker) 116 | 117 | let autocomplete = autocomplete worker 118 | let tooltip_on_hover = tooltip_on_hover worker 119 | let linter = linter worker 120 | let all_extensions = all_extensions worker 121 | end 122 | -------------------------------------------------------------------------------- /src/worker/worker.ml: -------------------------------------------------------------------------------- 1 | open Merlin_utils 2 | open Std 3 | open Js_of_ocaml 4 | open Merlin_kernel 5 | module Location = Ocaml_parsing.Location 6 | 7 | let stdlib_path = "/static/cmis" 8 | let log s = Console.console##log (Js.string s) 9 | 10 | let sync_get url = 11 | let x = XmlHttpRequest.create () in 12 | x##.responseType := Js.string "arraybuffer"; 13 | x##_open (Js.string "GET") (Js.string url) Js._false; 14 | x##send Js.null; 15 | match x##.status with 16 | | 200 -> 17 | Js.Opt.case 18 | (File.CoerceTo.arrayBuffer x##.response) 19 | (fun () -> 20 | Console.console##log (Js.string "Failed to receive file"); 21 | None) 22 | (fun b -> Some (Typed_array.String.of_arrayBuffer b)) 23 | | _ -> None 24 | 25 | let filename_of_module unit_name = 26 | Printf.sprintf "%s.cmi" (String.uncapitalize_ascii unit_name) 27 | 28 | let reset_dirs () = 29 | Ocaml_utils.Directory_content_cache.clear (); 30 | let open Ocaml_utils.Load_path in 31 | let { visible; hidden } = get_paths () in 32 | reset (); 33 | init ~auto_include:no_auto_include ~visible ~hidden 34 | 35 | let add_dynamic_cmis dcs = 36 | let open Ocaml_typing.Persistent_env.Persistent_signature in 37 | let old_loader = !load in 38 | 39 | let fetch = 40 | (fun filename -> 41 | let url = Filename.concat dcs.Protocol.dcs_url filename in 42 | sync_get url) 43 | in 44 | 45 | List.iter ~f:(fun name -> 46 | let filename = filename_of_module name in 47 | match fetch (filename_of_module name) with 48 | | Some content -> 49 | let name = Filename.(concat stdlib_path filename) in 50 | Sys_js.create_file ~name ~content 51 | | None -> ()) dcs.dcs_toplevel_modules; 52 | 53 | let new_load ~allow_hidden ~unit_name = 54 | let filename = filename_of_module unit_name in 55 | let fs_name = Filename.(concat stdlib_path filename) in 56 | (* Check if it's already been downloaded. This will be the 57 | case for all toplevel cmis. Also check whether we're supposed 58 | to handle this cmi *) 59 | if 60 | not (Sys.file_exists fs_name) && 61 | List.exists ~f:(fun prefix -> 62 | String.starts_with ~prefix filename) dcs.dcs_file_prefixes 63 | then begin 64 | match fetch filename with 65 | | Some x -> 66 | Sys_js.create_file ~name:fs_name ~content:x; 67 | (* At this point we need to tell merlin that the dir contents 68 | have changed *) 69 | reset_dirs () 70 | | None -> 71 | Printf.eprintf "Warning: Expected to find cmi at: %s\n%!" 72 | (Filename.concat dcs.Protocol.dcs_url filename) 73 | end; 74 | old_loader ~allow_hidden ~unit_name 75 | in 76 | load := new_load 77 | 78 | let add_cmis { Protocol.static_cmis; dynamic_cmis } = 79 | List.iter static_cmis ~f:(fun { Protocol.sc_name; sc_content } -> 80 | let filename = Printf.sprintf "%s.cmi" (String.uncapitalize_ascii sc_name) in 81 | let name = Filename.(concat stdlib_path filename) in 82 | Sys_js.create_file ~name ~content:sc_content); 83 | Option.iter ~f:add_dynamic_cmis dynamic_cmis; 84 | Protocol.Added_cmis 85 | 86 | let config = 87 | let initial = Mconfig.initial in 88 | { initial with 89 | merlin = { initial.merlin with 90 | stdlib = Some stdlib_path }} 91 | 92 | let make_pipeline source = 93 | Mpipeline.make config source 94 | 95 | let dispatch source query = 96 | let pipeline = make_pipeline source in 97 | Mpipeline.with_pipeline pipeline @@ fun () -> ( 98 | Query_commands.dispatch pipeline query 99 | ) 100 | 101 | module Completion = struct 102 | (* Prefixing code from ocaml-lsp-server *) 103 | let rfindi = 104 | let rec loop s ~f i = 105 | if i < 0 then 106 | None 107 | else if f (String.unsafe_get s i) then 108 | Some i 109 | else 110 | loop s ~f (i - 1) 111 | in 112 | fun ?from s ~f -> 113 | let from = 114 | let len = String.length s in 115 | match from with 116 | | None -> len - 1 117 | | Some i -> 118 | if i > len - 1 then 119 | raise @@ Invalid_argument "rfindi: invalid from" 120 | else 121 | i 122 | in 123 | loop s ~f from 124 | let lsplit2 s ~on = 125 | match String.index_opt s on with 126 | | None -> None 127 | | Some i -> 128 | let open String in 129 | Some (sub s ~pos:0 ~len:i, sub s ~pos:(i + 1) ~len:(length s - i - 1)) 130 | 131 | (** @see