├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── api.ex ├── app.ex ├── host.ex ├── link.ex ├── logger.ex └── plugin.ex ├── mix.exs ├── mix.lock ├── test ├── link_test.exs ├── neovim_test.exs └── test_helper.exs └── vim-elixir-host └── plugin └── elixir_host.vim /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /vim-elixir-host/tools/nvim_elixir_host 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Elixir host for NVim 2 | ==================== 3 | 4 | Instead of this repository, you can directly use 5 | https://github.com/awetzel/elixir.nvim, which packages this host, 6 | a vim plugin with useful functions `awetzel/nvim-rplugin`, and 7 | add some vim configuration. 8 | 9 | # Write your Vim plugin in Elixir : Elixir Host for NVim # 10 | 11 | Firstly, to replace your vim with nvim, not so hard :) 12 | 13 | ``` 14 | git clone https://github.com/neovim/neovim ; cd neovim ; sudo make install 15 | # add-apt-repository ppa:neovim-ppa/unstable && apt-get update && apt-get install neovimapt-get install 16 | cp -R ~/.vim ~/.config/nvim ; cp ~/.vimrc ~/.config/nvim/init.vim 17 | alias vim=nvim 18 | ``` 19 | 20 | ## INSTALL this host ## 21 | 22 | Compile the Elixir Host, then copy the vim-elixir-host directory to `~/.nvim` : 23 | 24 | ``` 25 | mix deps.get 26 | MIX_ENV=host mix escript.build 27 | cp -R vim-elixir-host/* ~/.nvim/ 28 | # or with pathogen cp -R vim-elixir-host ~/.nvim/bundle/ 29 | ``` 30 | 31 | That's it ! 32 | 33 | You can also use `MIX_ENV=debug_host` to compile a host plugin which 34 | logs into a `./nvim_debug` file and set log level to `:debug` (see below). 35 | 36 | ## Write a vim Elixir plugin ## 37 | 38 | Before going into a detail, let's see a basic usage example : add 39 | Elixir autocompletion for module and functions, with documentation in 40 | the preview window, in less than 40 LOC. 41 | 42 | ``` 43 | mkdir -p ~/.nvim/rplugin/elixir 44 | vim ~/.nvim/rplugin/elixir/completion.ex 45 | ``` 46 | 47 | ```elixir 48 | defmodule AutoComplete do 49 | use NVim.Plugin 50 | 51 | deffunc elixir_complete("1",_,cursor,line,state), eval: "col('.')", eval: "getline('.')" do 52 | cursor = cursor - 1 # because we are in insert mode 53 | [tomatch] = Regex.run(~r"[\w\.:]*$",String.slice(line,0..cursor-1)) 54 | cursor - String.length(tomatch) 55 | end 56 | deffunc elixir_complete(_,base,_,_,state), eval: "col('.')", eval: "getline('.')" do 57 | case (base |> to_char_list |> Enum.reverse |> IEx.Autocomplete.expand) do 58 | {:no,_,_}-> [base] # no expand 59 | {:yes,comp,[]}->["#{base}#{comp}"] #simple expand, no choices 60 | {:yes,_,alts}-> # multiple choices 61 | Enum.map(alts,fn comp-> 62 | {base,comp} = {String.replace(base,~r"[^.]*$",""), to_string(comp)} 63 | case Regex.run(~r"^(.*)/([0-9]+)$",comp) do # first see if these choices are module or function 64 | [_,function,arity]-> # it is a function completion 65 | replace = base<>function 66 | module = if String.last(base) == ".", do: Module.concat([String.slice(base,0..-2)]), else: Kernel 67 | if (docs=Code.get_docs(module,:docs)) && (doc=List.keyfind(docs,{:"#{function}",elem(Integer.parse(arity),0)},0)) && (docmd=elem(doc,4)) do 68 | %{"word"=>replace,"kind"=> if(elem(doc,2)==:def, do: "f", else: "m"), "abbr"=>comp,"info"=>docmd} 69 | else 70 | %{"word"=>replace,"abbr"=>comp} 71 | end 72 | nil-> # it is a module completion 73 | module = base<>comp 74 | case Code.get_docs(Module.concat([module]),:moduledoc) do 75 | {_,moduledoc} -> %{"word"=>module,"info"=>moduledoc} 76 | _ -> %{"word"=>module} 77 | end 78 | end 79 | end) 80 | end 81 | end 82 | 83 | defautocmd file_type(state), pattern: "elixir", async: true do 84 | {:ok,nil} = NVim.vim_command("filetype plugin on") 85 | {:ok,nil} = NVim.vim_command("set omnifunc=ElixirComplete") 86 | state 87 | end 88 | end 89 | ``` 90 | 91 | And then open nvim and execute `:UpdateRemotePlugins` to update the plugin database. 92 | 93 | That's it, just open an elixir file and "CTRL-X CTRL-O" for completion. 94 | 95 | ## Write a compiled vim Elixir plugin 96 | 97 | Create any OTP app with a *plugin_module* (as described below) inside it. 98 | Then create an erlang archive and put it into your `rplugin/elixir` directory. 99 | 100 | ```bash 101 | mix new myplugin 102 | cd myplugin 103 | vim lib/myplugin.ex 104 | # write your plugin module, like the AutoComplete module below 105 | mix archive.build 106 | cp myplugin-0.0.1.ez ~/.config/nvim/rplugin/elixir/ 107 | ``` 108 | 109 | ## Plugin architecture ## 110 | 111 | But the integration allows much more things, lets look into 112 | details : 113 | 114 | - A plugin is either: 115 | - an elixir file defining modules in `RUNTIMEPATH/rplugin/elixir`, 116 | but only one module must implement the `nvim_specs` function, 117 | it is called the _plugin module_ 118 | - an archive `someapp.ez` in `RUNTIMEPATH/rplugin/elixir` 119 | containing an otp app, inside it there must be one and only one 120 | module implementing the `nvim_specs` function, it is called the 121 | _plugin module_ 122 | - The _plugin module_ must implement `child_spec/0` returning the 123 | supervisor child specification started on the first plugin call. 124 | - The supervision tree must launch in it a GenServer registered 125 | with the name of the _plugin module_, a vim query will trigger a 126 | genserver call `{:function|:autocmd|:command,methodname,args}` 127 | to this registered process. 128 | - The _plugin module_ must implement `nvim_specs/0` returning the 129 | specification of available commands, functions, autocmd with 130 | their options, as describeb in nvim documentation, in order to 131 | define them on the vim side as rpc calls to the host plugin : 132 | (`UpdateRemotePlugins`). 133 | 134 | The code is self explanatory, so you can look at `host.ex` where 135 | you can see this architecture : 136 | 137 | ```elixir 138 | def ensure_plugin(path,plugins) do 139 | case plugins[path] do 140 | nil -> 141 | plugin = case Path.extname(path) do 142 | ".ex"-> 143 | modules = Code.compile_string(File.read!(path),path) |> Enum.map(&elem(&1,0)) 144 | Enum.find(modules,&function_exported?(&1,:nvim_specs,0)) 145 | ".ez"-> 146 | app_version = path |> Path.basename |> Path.rootname 147 | app = app_version |> String.replace(~r/-([0-9]+\.?)+/,"") |> String.to_atom 148 | Code.append_path("#{path}/#{app_version}/ebin") 149 | res = Application.ensure_all_started(app) 150 | {:ok,modules} = :application.get_key(app,:modules) 151 | Enum.each(modules,&Code.ensure_loaded/1) 152 | Application.get_env(app,:nvim_plugin) || ( 153 | {:ok,modules} = :application.get_key(app,:modules) 154 | Enum.find(modules,&function_exported?(&1,:nvim_specs,0))) 155 | end 156 | {:ok,_} = Supervisor.start_child NVim.Plugin.Sup, plugin.child_spec 157 | {plugin,Dict.put(plugins,path,plugin)} 158 | plugin -> {plugin,plugins} 159 | end 160 | end 161 | def specs(plugin), do: plugin.nvim_specs 162 | 163 | def handle(plugin,[type|name],args), do: 164 | GenServer.call(plugin,{:"#{type}",compose_name(name),args}) 165 | ``` 166 | 167 | ## Main plugin module definition facilities ## 168 | 169 | `Use NVim.plugin` provides facilities to define the previously described module : 170 | 171 | - `use NVim.plugin` : 172 | - define a GenServer (`use GenServer`) with a `start_link` 173 | function starting it registered with the _plugin module_ name. 174 | - define a default but overridable `child_spec` launching only 175 | this GenServer. 176 | - define at the end of the module the `nvim_specs` function returning `@specs` 177 | - import macros`deffunc`,`defcommand`,`defautocmd` which 178 | - adds a nvim specification to `@spec` 179 | - defines a `def handle_call` but rearranging parameters and 180 | wrapping response to make it easier to understand and makes 181 | its definition closer to the corresponding vim definition. 182 | 183 | So in the end `deffunc|defcommand|defautocmd` are only `def 184 | handle_call` so you can pattern match and add guards as you want 185 | (see the example of the completion function above). You can add 186 | `handle_info`, `handle_cast` or even additional `handle_call` if 187 | needed. You can customize the `child_spec` in order to launch 188 | dependencies, with the only contraint that the new tree must 189 | contains the _plugin module_ GenServer. 190 | 191 | ## Understand deffunc ## 192 | 193 | Todo 194 | 195 | ## Understand defcommand ## 196 | 197 | Todo 198 | 199 | ## Understand defautocmd ## 200 | 201 | Todo 202 | 203 | ## Debugging and Logger 204 | 205 | Standard output and input of the neovim host are used to communicate with vim, so 206 | to avoid any freeze, the erlang `group_leader` (pid where io outputs are send 207 | through a protocol), is set to a *sink*, so all outputs are ignored. 208 | 209 | To allow some debugging and feed back from your plugin, two `Logger` backends 210 | are provided: 211 | 212 | - `NVim.Logger` takes the first line of a log and `echo` it to vim. 213 | - `NVim.DebugLogger` append log to a "./nvim_debug" file (configurable with `:debug_logger_file` env) 214 | 215 | # Control a nvim instance from Elixir # 216 | 217 | Connect to a running vim instance using : 218 | 219 | ``` 220 | iex -S mix nvim.attach "127.0.0.1:7777" 221 | iex -S mix nvim.attach "[::1]:7777" 222 | iex -S mix nvim.attach "/path/to/unix/domain/sock" 223 | ``` 224 | 225 | The argument is where the socket of your nvim instance lies : to 226 | find the current listening socket of your nvim instance, just 227 | read the correct env variable : 228 | 229 | ``` 230 | "" in vim 231 | :echo $NVIM_LISTEN_ADDRESS 232 | ``` 233 | 234 | By default this socket is a unix domain socket in a random file, 235 | but you can customize the address at launch (tcp or unix domain socket): 236 | 237 | ``` 238 | NVIM_LISTEN_ADDRESS="127.0.0.1:7777" nvim 239 | NVIM_LISTEN_ADDRESS="/tmp/mysock" nvim 240 | ``` 241 | 242 | ## Auto generated API ## 243 | 244 | The module `NVim` is automatically generated when you attach to 245 | vim using `vim_get_api_info`. 246 | 247 | ``` 248 | {:ok,current_line} = NVim.vim_get_current_line 249 | {:ok,current_column} = NVim.vim_eval "col('.')" 250 | NVim.vim_command "echo 'coucou'" 251 | ``` 252 | 253 | The help is also automatically generated 254 | 255 | ``` 256 | h NVim.vim_del_current_line 257 | ``` 258 | 259 | 260 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | backends: [case Mix.env do 5 | :host-> NVim.Logger 6 | :debug_host-> NVim.DebugLogger 7 | _-> :console 8 | end], 9 | level: if(Mix.env == :debug_host, do: :debug, else: :info), 10 | handle_otp_reports: true, 11 | handle_sasl_reports: true 12 | 13 | config :neovim, 14 | debug_logger_file: "nvim_debug", 15 | update_api_on_startup: true, 16 | link: if(Mix.env in [:host,:debug_host], 17 | do: :stdio, 18 | else: {:tcp,"127.0.0.1",6666}) 19 | -------------------------------------------------------------------------------- /lib/api.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.Api do 2 | @moduledoc """ 3 | Auto generate the NVim module with functions extracted from the spec 4 | available either globally using `nvim --api-info` or once an instance is 5 | attached with the `vim_get_api_info` internal cmd. 6 | """ 7 | 8 | def from_cmd do 9 | case System.cmd("nvim",["--api-info"]) do 10 | {res,0} -> 11 | {:ok,spec} = MessagePack.unpack res 12 | generate_neovim(spec) 13 | _ -> :ok 14 | end 15 | end 16 | 17 | def from_instance do 18 | {:ok,[_,spec]} = GenServer.call NVim.Link, {"vim_get_api_info",[]} 19 | generate_neovim(spec) 20 | end 21 | 22 | ## HACK TO ENSURE that Vim function name IS NOT a reserved elixir keyword 23 | def map_vimname(:fn), do: :fun 24 | def map_vimname(param), do: param 25 | 26 | def generate_neovim(%{"functions"=>fns,"types"=>types}) do 27 | defmodule Elixir.NVim do 28 | Enum.each fns, fn %{"name"=>name,"parameters"=>params}=func-> 29 | fnparams = for [_type,pname]<-params,do: quote(do: var!(unquote({NVim.Api.map_vimname(:"#{pname}"),[],Elixir}))) 30 | @doc """ 31 | Parameters : #{inspect params} 32 | 33 | Return : #{inspect func["return_type"]} 34 | 35 | This function can #{if func["can_fail"]!=true, do: "not "}fail 36 | 37 | This function can #{if func["deferred"]!=true, do: "not "}be deferred 38 | """ 39 | Module.eval_quoted(NVim, quote do 40 | def unquote(:"#{name}")(unquote_splicing(fnparams)) do 41 | GenServer.call NVim.Link, {unquote("#{name}"),unquote(fnparams)}, :infinity 42 | end 43 | end) 44 | end 45 | end 46 | Enum.each types, fn {name,%{"id"=>_id}}-> 47 | defmodule Module.concat(["Elixir","NVim",name]) do 48 | defstruct content: "" 49 | end 50 | end 51 | defmodule Elixir.NVim.Ext do 52 | use MessagePack.Ext.Behaviour 53 | Enum.each types, fn {name,%{"id"=>id}}-> 54 | Module.eval_quoted(NVim.Ext, quote do 55 | def pack(%unquote(Module.concat(["NVim",name])){content: bin}), do: 56 | {:ok, {unquote(id),bin}} 57 | end) 58 | end 59 | Enum.each types, fn {name,%{"id"=>id}}-> 60 | Module.eval_quoted(NVim.Ext, quote do 61 | def unpack(unquote(id),bin), do: 62 | {:ok, %unquote(Module.concat(["NVim",name])){content: bin}} 63 | end) 64 | end 65 | end 66 | end 67 | end 68 | 69 | NVim.Api.from_cmd 70 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.App do 2 | use Application 3 | def start(_type, _args) do 4 | ## Make sure that the IO server of this application and all launched applications 5 | ## is a sink ignoring messages, to ensure that standard input/output will 6 | ## not be taken by running code and make the neovim host die 7 | io_sink = IOLeaderSink.start_link 8 | Process.group_leader(self(),io_sink) 9 | Process.group_leader(Process.whereis(:application_controller),io_sink) 10 | 11 | unquote(if Mix.env !== :test, 12 | do: quote(do: NVim.App.Sup.start_link), 13 | else: quote(do: {:ok,spawn fn->:ok end})) 14 | end 15 | 16 | defmodule Sup do 17 | use Supervisor 18 | 19 | def start_link, do: Supervisor.start_link(__MODULE__,[]) 20 | 21 | def init([]) do 22 | supervise([ 23 | worker(NVim.Link,[Application.get_env(:neovim,:link,:stdio)]), 24 | worker(NVim.Plugin.Sup,[]), 25 | worker(__MODULE__,[], function: :gen_api, restart: :temporary) 26 | ], strategy: :one_for_all) 27 | end 28 | 29 | def gen_api do 30 | if Application.get_env(:neovim,:update_api_on_startup,true), do: 31 | NVim.Api.from_instance 32 | :ignore 33 | end 34 | end 35 | end 36 | 37 | defmodule Sleeper do 38 | @moduledoc "when start as escript, just wait" 39 | def main(_), do: :timer.sleep(:infinity) 40 | end 41 | 42 | defmodule Mix.Tasks.Nvim.Attach do 43 | use Mix.Task 44 | 45 | def run([arg]) do 46 | Mix.Task.run "loadconfig", [] 47 | if ?/ in '#{arg}' do 48 | :application.set_env(:neovim,:link,{:unix,arg}, persistent: true) 49 | else 50 | [_,ip,port] = Regex.run ~r/^\[?(.*)\]?:([0-9]+)$/, arg 51 | {port,_} = Integer.parse(port) 52 | :application.set_env(:neovim,:link,{:tcp,ip,port}, persistent: true) 53 | end 54 | Mix.Task.run "app.start", [] 55 | end 56 | def run(_) do 57 | Mix.shell.info "usage : " 58 | Mix.shell.info "iex -S mix nvim.attach /path/to/unix/socket" 59 | Mix.shell.info "iex -S mix nvim.attach ip4:port" 60 | Mix.shell.info "iex -S mix nvim.attach ip6:port" 61 | end 62 | end 63 | 64 | defmodule IOLeaderSink do 65 | def handle(from,reply_as,{:put_chars,_,_}), do: 66 | send(from,{:io_reply,reply_as,:ok}) 67 | def handle(from,reply_as,{:put_chars,_,_,_,_}), do: 68 | send(from,{:io_reply,reply_as,:ok}) 69 | def handle(from,reply_as,{:get_until,_,_,_,_,_}), do: 70 | send(from,{:io_reply,reply_as,{:done,:eof,[]}}) 71 | def handle(from,reply_as,{:get_chars,_,_,_}), do: 72 | send(from,{:io_reply,reply_as,:eof}) 73 | def handle(from,reply_as,{:get_line,_,_}), do: 74 | send(from,{:io_reply,reply_as,:eof}) 75 | def handle(from,reply_as,{:setopts,_}), do: 76 | send(from,{:io_reply,reply_as,:ok}) 77 | def handle(from,reply_as,:getopts), do: 78 | send(from,{:io_reply,reply_as,[]}) 79 | def handle(from,reply_as,{:requests,requests}), do: 80 | for(r<-requests, do: handle(from,reply_as,r)) 81 | def handle(from,reply_as,_), do: 82 | send(from,{:io_reply,reply_as,{:error,:request}}) 83 | def loop do 84 | receive do 85 | {:io_request,from,reply_as,req}-> handle(from,reply_as,req) 86 | _->:ok 87 | end; loop() 88 | end 89 | def start_link, do: spawn_link(__MODULE__,:loop,[]) 90 | end 91 | -------------------------------------------------------------------------------- /lib/host.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.Host do 2 | def init_plugins, do: HashDict.new 3 | def ensure_plugin(path,plugins) do 4 | case plugins[path] do 5 | nil -> 6 | plugin = case Path.extname(path) do 7 | ".ex"-> 8 | modules = Code.compile_string(File.read!(path),path) |> Enum.map(&elem(&1,0)) 9 | Enum.find(modules,&function_exported?(&1,:nvim_specs,0)) 10 | ".ez"-> 11 | app_version = path |> Path.basename |> Path.rootname 12 | app = app_version |> String.replace(~r/-([0-9]+\.?)+/,"") |> String.to_atom 13 | Code.append_path("#{path}/#{app_version}/ebin") 14 | res = Application.ensure_all_started(app) 15 | {:ok,modules} = :application.get_key(app,:modules) 16 | Enum.each(modules,&Code.ensure_loaded/1) 17 | Application.get_env(app,:nvim_plugin) || ( 18 | {:ok,modules} = :application.get_key(app,:modules) 19 | Enum.find(modules,&function_exported?(&1,:nvim_specs,0))) 20 | end 21 | {:ok,_} = Supervisor.start_child NVim.Plugin.Sup, plugin.child_spec 22 | {plugin,Dict.put(plugins,path,plugin)} 23 | plugin -> {plugin,plugins} 24 | end 25 | end 26 | 27 | def specs(plugin), do: plugin.nvim_specs 28 | 29 | def compose_name([simple_name]), do: simple_name 30 | def compose_name(composed_name), do: List.to_tuple(composed_name) 31 | def handle(plugin,[type|name],args) do 32 | GenServer.call(plugin,{:"#{type}",compose_name(name),args}, :infinity) 33 | end 34 | end 35 | 36 | defmodule NVim.Plugin.Sup do 37 | use Supervisor 38 | def start_link, do: Supervisor.start_link(__MODULE__,nil,name: __MODULE__) 39 | def init(_), do: supervise([], strategy: :one_for_one) 40 | end 41 | -------------------------------------------------------------------------------- /lib/link.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.Link do 2 | use GenServer 3 | require Logger 4 | alias :procket, as: Socket 5 | @msg_req 0 6 | @msg_resp 1 7 | @msg_notify 2 8 | 9 | def start_link(link_spec), 10 | do: GenServer.start_link(__MODULE__,link_spec, name: __MODULE__) 11 | 12 | def init(link_spec) do 13 | Process.flag(:trap_exit,true) 14 | {fdin,fdout} = open(link_spec) 15 | port = Port.open({:fd,fdin,fdout}, [:stream,:binary]) 16 | Process.link(port) 17 | {:ok,%{link_spec: link_spec, port: port, buf: "", req_id: 0, reqs: HashDict.new, plugins: NVim.Host.init_plugins}} 18 | end 19 | 20 | def handle_call({func,args},from,%{port: port}=state) do 21 | req_id = state.req_id+1 22 | Port.command port, MessagePack.pack!([@msg_req,req_id,func,args], ext: NVim.Ext) 23 | {:noreply,%{state|req_id: req_id, reqs: Dict.put(state.reqs,req_id,from)}} 24 | end 25 | def handle_cast({:register_plugins,plugins},state) do 26 | {:noreply,%{state|plugins: plugins}} 27 | end 28 | 29 | defp reply(port,id,{:ok,res}) do 30 | Port.command port, MessagePack.pack!([@msg_resp,id,nil,res]) 31 | end 32 | defp reply(port,id,{:error,err}) do 33 | Port.command port, MessagePack.pack!([@msg_resp,id,err,nil]) 34 | end 35 | defp reply(port,id,res), do: reply(port,id,{:ok,res}) 36 | 37 | def parse_msgs(data,acc) do 38 | case MessagePack.unpack_once(data, enable_string: true, ext: NVim.Ext) do 39 | {:ok,{msg,tail}}->parse_msgs(tail,[msg|acc]) 40 | {:error,_}->{data,Enum.reverse(acc)} 41 | end 42 | end 43 | 44 | def handle_info({port,{:data,data}},%{reqs: reqs,buf: buf,plugins: plugins}=state) do 45 | {tail,msgs} = parse_msgs(buf<>data,[]) 46 | {reqs,plugins} = Enum.reduce(msgs,{reqs,plugins},fn 47 | [@msg_resp,req_id,err,resp], {reqs,plugins}-> 48 | reply = if err, do: {:error,err}, else: {:ok, resp} 49 | reqs=case Dict.pop(reqs,req_id) do 50 | {nil,_} -> reqs 51 | {reply_to,reqs} -> GenServer.reply(reply_to,reply); reqs 52 | end 53 | {reqs,plugins} 54 | [@msg_req,req_id,method,args], {_,plugins}=acc-> 55 | spawn fn-> 56 | try do 57 | case String.split(method,":") do 58 | ["poll"]-> reply(port,req_id, {:ok,"ok"}) 59 | ["specs"]-> 60 | {plugin,plugins} = NVim.Host.ensure_plugin(hd(args),plugins) 61 | reply port,req_id, {:ok,NVim.Host.specs(plugin)} 62 | GenServer.cast __MODULE__,{:register_plugins,plugins} 63 | [path|methodpath]-> 64 | {plugin,plugins} = NVim.Host.ensure_plugin(path,plugins) 65 | GenServer.cast __MODULE__,{:register_plugins,plugins} 66 | r = NVim.Host.handle(plugin,methodpath,args) 67 | reply port,req_id, r 68 | end 69 | catch _, r -> 70 | reply port,req_id, {:error,inspect(r)} 71 | end 72 | end 73 | acc 74 | [@msg_notify,method,args], {reqs,plugins}-> 75 | [path|methodpath] = String.split(method,":") 76 | {plugin,plugins} = NVim.Host.ensure_plugin(path,plugins) 77 | spawn fn-> 78 | try do 79 | NVim.Host.handle(plugin,methodpath,args) 80 | catch _, r -> 81 | Logger.error "failed to exec autocmd #{hd(methodpath)} : #{inspect r}" 82 | end 83 | end 84 | {reqs,plugins} 85 | end) 86 | {:noreply,%{state|buf: tail, reqs: reqs, plugins: plugins}} 87 | end 88 | def handle_info({:EXIT,port,_},%{port: port,link_spec: :stdio}=state) do 89 | System.halt(0) # if the port die in stdio mode, it means the link is broken, kill the app 90 | {:noreply,state} 91 | end 92 | def handle_info({:EXIT,port,reason},%{port: port}=state) do 93 | {:stop,reason,state} 94 | end 95 | 96 | def terminate(_reason,state) do 97 | Port.close(state.port) 98 | catch _, _ -> :ok 99 | end 100 | 101 | defp parse_ip(ip) do 102 | case :inet.parse_address('#{ip}') do 103 | {:ok,{ip1,ip2,ip3,ip4}}-> 104 | {:ipv4,<>} 105 | {:ok,{ip1,ip2,ip3,ip4,ip5,ip6,ip7,ip8}}-> 106 | {:ipv6,<>} 107 | end 108 | end 109 | 110 | defp open(:stdio), do: {0,1} 111 | defp open({:tcp,ip,port}), do: 112 | open({parse_ip(ip),port}) 113 | defp open({{:ipv4,ip},port}) do 114 | sockaddr = Socket.sockaddr_common(2,6)<> <> 115 | open({:sock,2,sockaddr}) 116 | end 117 | defp open({{:ipv6,ip},port}) do 118 | sockaddr = Socket.sockaddr_common(30,26)<> <> 119 | open({:sock,30,sockaddr}) 120 | end 121 | defp open({:unix,sockpath}) do 122 | pad = 8*(Socket.unix_path_max - byte_size(sockpath)) 123 | sockaddr = Socket.sockaddr_common(1,byte_size(sockpath)) <> sockpath <> <<0::size(pad)>> 124 | open({:sock,1,sockaddr}) 125 | end 126 | defp open({:sock,family,sockaddr}) do 127 | {:ok,socket}= Socket.socket(family,1,0) 128 | case Socket.connect(socket,sockaddr) do 129 | r when r in [:ok,{:error,:einprogress}]->:ok 130 | end 131 | {socket,socket} 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.Logger do 2 | use GenEvent 3 | 4 | def handle_event({level,_leader,{Logger,msg,_ts,_md}},:activated) do 5 | NVim.vim_command ~s/echomsg "#{level}: #{clean_msg msg}"/ 6 | {:ok,:activated} 7 | end 8 | def handle_event({_,_,{Logger,["Application ","neovim"," started at "|_],_,_}},_), do: {:ok,:activated} 9 | def handle_event(_,state), do: {:ok,state} 10 | 11 | defp clean_msg(msg) do 12 | msg 13 | |> to_string 14 | |> String.split("\n") 15 | |> hd 16 | |> String.replace("\"","\\\"") 17 | end 18 | 19 | def handle_call({:configure,_opts},state), do: 20 | {:ok,:ok,state} 21 | end 22 | 23 | defmodule NVim.DebugLogger do 24 | use GenEvent 25 | @format Logger.Formatter.compile("$time $metadata[$level] $levelpad$message\n") 26 | 27 | def handle_event({level,_leader,{Logger,msg,ts,_md}},state) do 28 | file = Application.get_env(:neovim,:debug_logger_file,"nvim_debug") 29 | res = File.write(file,Logger.Formatter.format(@format,level,msg,ts,[]), [:append]) 30 | {:ok,state} 31 | end 32 | def handle_event(_,state), do: {:ok,state} 33 | 34 | def handle_call({:configure,_opts},state), do: 35 | {:ok,:ok,state} 36 | end 37 | -------------------------------------------------------------------------------- /lib/plugin.ex: -------------------------------------------------------------------------------- 1 | defmodule NVim.Plugin do 2 | 3 | defmacro __using__(_) do 4 | quote do 5 | import NVim.Plugin 6 | import Supervisor.Spec 7 | use GenServer 8 | @before_compile NVim.Plugin 9 | @specs %{} 10 | 11 | def start_link(init), do: GenServer.start_link(__MODULE__,init, name: __MODULE__) 12 | def child_spec, do: worker(__MODULE__,[%{}]) 13 | 14 | defoverridable [child_spec: 0] 15 | end 16 | end 17 | 18 | defmacro __before_compile__(_env) do 19 | quote do 20 | def handle_call({type,name,_},_,state) do 21 | {:reply,{:error,"no matching arguments for #{type} #{inspect name}"},state} 22 | end 23 | def nvim_specs, do: Dict.values(@specs) 24 | end 25 | end 26 | 27 | def eval_specs(params), do: 28 | Enum.filter(params,fn {k,_}-> k in [:eval] end) 29 | def autocmd_specs(params), do: 30 | Enum.filter(params,fn {k,_}-> k in [:group,:nested] end) 31 | def command_other_specs(params), do: 32 | Enum.filter(params,fn {k,_}-> k in [:buffer,:bar] end) 33 | def command_values_specs(params), do: 34 | Enum.filter(params,fn {k,_}-> k in [:range,:count,:bang,:register] end) 35 | def command_args_specs(params) do 36 | Enum.filter(params,fn {k,_}-> k in [:range,:count,:bang,:register,:eval] end) 37 | end 38 | 39 | def wrap_reply({status,reply,state},_,_) when status in [:ok,:error], do: 40 | {:reply,{status,reply},state} 41 | def wrap_reply(other,initstate,_), do: 42 | {:reply,{:ok,other},initstate} 43 | 44 | defp list_or_noarg([]), do: [] 45 | defp list_or_noarg(args), do: [args] 46 | 47 | def sig_to_sigwhen({:when,_,[{name,_,params},guard]}), do: {name,params,guard} 48 | def sig_to_sigwhen({name,_,params}), do: {name,params,true} 49 | 50 | defmacro deffunc(signature, funcparams \\ [], [do: body]) do 51 | {name,params,guard} = sig_to_sigwhen(signature) 52 | eval_specs = eval_specs(funcparams) 53 | [state|params] = Enum.reverse(params) 54 | 55 | {eval_args,params} = Enum.split(params,length(eval_specs)) 56 | eval_args = list_or_noarg(Enum.reverse(eval_args)) 57 | 58 | nargs_args = [Enum.reverse(params)] 59 | 60 | call_args = Enum.concat([nargs_args,eval_args]) 61 | name = Mix.Utils.camelize("#{name}") 62 | quote do 63 | @specs if(@specs[unquote(name)], do: @specs, else: 64 | Dict.put(@specs,unquote(name),%{ 65 | type: "function", 66 | name: unquote(name), 67 | sync: unquote(if(funcparams[:async], do: 0,else: 1)), 68 | opts: %{unquote_splicing(if eval_specs == [],do: [], 69 | else: [eval: "[#{eval_specs|>Dict.values|>Enum.join(",")}]"])} 70 | })) 71 | def handle_call({:function,unquote(name),unquote(call_args)},var!(nvim_from),unquote(state)=initialstate) when unquote(guard) do 72 | wrap_reply(unquote(body),initialstate,unquote(funcparams[:async] in [nil,false])) 73 | end 74 | end 75 | end 76 | 77 | defp wrap_spec_value(_,true), do: "" 78 | defp wrap_spec_value(:range,:default_all), do: "%" 79 | defp wrap_spec_value(:range,:default_line), do: "" 80 | defp wrap_spec_value(_,value), do: value 81 | 82 | defmacro defcommand(signature, funcparams \\ [], [do: body]) do 83 | {name,params,guard} = sig_to_sigwhen(signature) 84 | values_specs = command_values_specs(funcparams) 85 | eval_specs = eval_specs(funcparams) 86 | [state|params] = Enum.reverse(params) 87 | 88 | eval_indexes = for {{:eval,_},i}<-Enum.with_index(funcparams), do: i 89 | {special_eval_args,params} = Enum.split(params,length(values_specs) + length(eval_specs)) 90 | special_eval_args = Enum.reverse(special_eval_args) 91 | eval_args = Enum.map(eval_indexes,&Enum.at(special_eval_args,&1)) 92 | eval_args = list_or_noarg(eval_args) 93 | 94 | special_args = Enum.reduce(eval_indexes,special_eval_args,&List.delete_at(&2,&1)) 95 | special_dict = Enum.zip(Dict.keys(values_specs),special_args) 96 | nvim_arg_order = [range: 0,count: 1,bang: 2,register: 3] 97 | special_args = special_dict |> Enum.sort_by(fn {k,_}->nvim_arg_order[k] end) |> Dict.values 98 | 99 | with_defaults = Enum.reverse(params) 100 | without_defaults = Enum.map(with_defaults, fn {:\\,_,[e,_]}->e; e->e end) 101 | defaults = Enum.map(with_defaults,fn {:\\,_,[_,default]}->default; _->nil end) 102 | nargs_args = list_or_noarg(without_defaults) 103 | 104 | call_args = Enum.concat([nargs_args,special_args,eval_args]) 105 | default_call_args = Enum.concat([[quote do: _],special_args,eval_args]) 106 | name = Mix.Utils.camelize("#{name}") 107 | quote do 108 | @specs if(@specs[unquote(name)], do: @specs, else: 109 | Dict.put(@specs,unquote(name),%{ 110 | type: "command", 111 | name: unquote(name), 112 | sync: unquote(if(funcparams[:async], do: 0,else: 1)), 113 | opts: %{unquote_splicing( 114 | if(nargs_args == [], do: [], else: [nargs: "*"]) ++ 115 | Enum.map(values_specs++command_other_specs(funcparams),fn {k,v}->{k,wrap_spec_value(k,v)} end) ++ 116 | if(eval_specs == [], do: [], else: [eval: "[#{eval_specs|>Dict.values|>Enum.join(",")}]"]) 117 | )} 118 | })) 119 | def handle_call({:command,unquote(name),unquote(call_args)},var!(nvim_from),unquote(state)=initialstate) when unquote(guard) do 120 | wrap_reply(unquote(body),initialstate,unquote(funcparams[:async] in [nil,false])) 121 | end 122 | if unquote(nargs_args !== []) do 123 | def handle_call({:command,unquote(name),unquote(default_call_args)=[nargs|other_args]},from,unquote(state)=initialstate) when unquote(guard) do 124 | nargs = nargs ++ Enum.slice(unquote(defaults),length(nargs)..-1) 125 | handle_call({:command,unquote(name),[nargs|other_args]},from,initialstate) 126 | end 127 | end 128 | end 129 | end 130 | 131 | defmacro defautocmd(signature, funcparams \\ [], [do: body]) do 132 | {name,params,guard} = sig_to_sigwhen(signature) 133 | eval_specs = eval_specs(funcparams) 134 | [state|params] = Enum.reverse(params) 135 | 136 | call_args = list_or_noarg(Enum.reverse(params)) 137 | name = Mix.Utils.camelize("#{name}") 138 | pattern = funcparams[:pattern] || "*" 139 | quote do 140 | @specs if(@specs[unquote(name)], do: @specs, else: 141 | Dict.put(@specs,unquote(name),%{ 142 | type: "autocmd", 143 | name: unquote(name), 144 | sync: unquote(if(funcparams[:async], do: 0,else: 1)), 145 | opts: %{unquote_splicing( 146 | [pattern: pattern]++ 147 | Enum.map(autocmd_specs(funcparams),fn {k,v}->{k,wrap_spec_value(k,v)} end) ++ 148 | if(eval_specs == [],do: [], else: [eval: "[#{eval_specs|>Dict.values|>Enum.join(",")}]"]) 149 | )} 150 | })) 151 | def handle_call({:autocmd,{unquote(name),unquote(pattern)},unquote(call_args)},var!(nvim_from),unquote(state)=initialstate) when unquote(guard) do 152 | wrap_reply(unquote(body),initialstate,unquote(funcparams[:async] in [nil,false])) 153 | end 154 | end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NVim.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :neovim, 6 | version: "0.2.0", 7 | consolidate_protocols: false, 8 | elixir: "~> 1.0", 9 | escript: escript(), 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger,:mix,:eex,:ex_unit,:iex,:procket,:message_pack], 15 | mod: { NVim.App, [] }, 16 | env: [update_api_on_startup: true]] 17 | end 18 | 19 | defp deps do 20 | [{:message_pack, github: "awetzel/msgpack-elixir", branch: "unpack_map_as_map"}, 21 | {:procket, github: "msantos/procket"}] 22 | end 23 | 24 | defp escript, do: [ 25 | emu_args: "-noinput", 26 | path: "vim-elixir-host/tools/nvim_elixir_host", 27 | strip_beam: false, 28 | main_module: Sleeper 29 | ] 30 | 31 | end 32 | 33 | 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "message_pack": {:git, "https://github.com/awetzel/msgpack-elixir.git", "98e6e71a6de5d651759af673cf2130a4280dd0d8", [branch: "unpack_map_as_map"]}, 3 | "procket": {:git, "https://github.com/msantos/procket.git", "eddc2d965f9b14abd06677cb2d1cf9c2ee9759ee", []}, 4 | } 5 | -------------------------------------------------------------------------------- /test/link_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NVim.LinkTest do 2 | use ExUnit.Case 3 | alias NVim.Link 4 | 5 | test "unpack messages with long string" do 6 | message_pack_with_long_string = <<148, 0, 2, 164, 112, 111, 108, 108, 145, 145, 217, 32, 7 | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 8 | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97>> 9 | 10 | {_, parsed_message} = NVim.Link.parse_msgs(message_pack_with_long_string, []) 11 | 12 | assert parsed_message == [[0, 2, "poll", [["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]]]] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/neovim_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestPlugin do 2 | use NVim.Plugin 3 | 4 | defcommand cmd_n1(arg1,arg2 \\ "toto",bang,eval1,count,eval2,_), bang: true, eval: "xx", count: true, eval: "xx" do 5 | {arg1,arg2,bang,eval1,count,eval2} 6 | end 7 | 8 | deffunc fun1(arg1,arg2,eval1,eval2,_), eval: "xx", eval: "xx" do 9 | {arg1,arg2,eval1,eval2} 10 | end 11 | 12 | defautocmd auto_cmd1(eval1,eval2,_), pattern: "*.ex", eval: "xx", eval: "xx" do 13 | {eval1,eval2} 14 | end 15 | end 16 | defmodule NeovimTest do 17 | use ExUnit.Case 18 | 19 | test "defcommand param arrangement" do 20 | assert {:reply,{:ok,{:n1,:n2,1,"toto",34,3}},:state} = 21 | TestPlugin.handle_call({:command,"CmdN1", 22 | [[:n1,:n2],34,1,["toto",3]]},nil,:state) 23 | end 24 | 25 | test "defcommand default params" do 26 | assert {:reply,{:ok,{nil,"toto",1,"toto",34,3}},:state} = 27 | TestPlugin.handle_call({:command,"CmdN1",[[],34,1,["toto",3]]},nil,:state) 28 | end 29 | 30 | test "deffunc param arrangement" do 31 | assert {:reply,{:ok,{:n1,:n2,"toto",3}},:state} = 32 | TestPlugin.handle_call({:function,"Fun1", 33 | [[:n1,:n2],["toto",3]]},nil,:state) 34 | end 35 | 36 | test "defautocmd param arrangement" do 37 | assert {:reply,{:ok,{"toto",3}},:state} = 38 | TestPlugin.handle_call({:autocmd,{"AutoCmd1","*.ex"}, 39 | [["toto",3]]},nil,:state) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /vim-elixir-host/plugin/elixir_host.vim: -------------------------------------------------------------------------------- 1 | function! s:RequireElixirHost(host) 2 | try 3 | let channel_id = rpcstart(globpath(&rtp, 'tools/nvim_elixir_host'),[]) 4 | if rpcrequest(channel_id, 'poll') == 'ok' 5 | return channel_id 6 | endif 7 | catch 8 | endtry 9 | throw 'Failed to load elixir host.' . 10 | \ " Maybe Erlang is missing" 11 | endfunction 12 | 13 | call remote#host#Register('elixir', '*.e[zx]', function('s:RequireElixirHost')) 14 | --------------------------------------------------------------------------------