-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d22d129
Showing
29 changed files
with
1,386 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/**/**/_build | ||
/cover | ||
/**/**/deps | ||
/tmp | ||
/spec/fixtures/xdg_home/nvim/plugin/* | ||
/spec/fixtures/xdg_home/nvim/rplugin/* | ||
erl_crash.dump | ||
*.ez | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# Neovim Elixir host | ||
|
||
Implements support for Neovim remote plugins written in Elixir. | ||
|
||
|
||
## Installation | ||
|
||
Assume you are already dealing with a working Elixir install. | ||
|
||
Install the host archive, we will use it to build the host locally. | ||
|
||
``` | ||
$ mix archive.install https://github.com/dm1try/neovim_host/raw/master/nvim.ez | ||
``` | ||
|
||
Build and install the host by running `nvim.install` and providing the path to nvim config(`~/.config/nvim` by default on linux systems) | ||
|
||
``` | ||
$ mix nvim.install /path/to/nvim/config | ||
``` | ||
|
||
## Usage | ||
|
||
Currently, each time you somehow update remote plugins you should run `:UpdateElixirPlugins` nvim command(the wrapper for `:UpdateRemotePlugins`). See `:h remote-plugins-manifest` for the clarification why the manifest is needed(generally, it saves the neovim startup time if remote plugins are installed). | ||
|
||
# Plugin Development | ||
## Structure | ||
|
||
Host supports two types of plugins: | ||
1. Scripts (an elixir script that usually contains simple logic and does not depend on other libs/does not need the versioning/etc). | ||
|
||
2. Applications (an OTP application that is implemented as part of host umbrella project). You can find more information about umbrella projects [here](http://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html). | ||
|
||
Host with plugins lives in `rplugin/elixir` of neovim config directory. | ||
Typical files tree for such dir: | ||
```bash | ||
~/.config/nvim/rplugin/elixir | ||
|
||
├── scripts <~ scripts | ||
├── apps <~ applications AKA "precompiled plugins" | ||
└── mix.exs | ||
``` | ||
|
||
### Plugin DSL | ||
#### Events | ||
`on_event`(better known as `autocmd` for vim users) defines the callback that triggered by editor when some event | ||
happened. Run `:h autocmd-events` for the list of events. | ||
|
||
```elixir | ||
on_event :vim_enter do | ||
Logger.info("the editor is ready") | ||
end | ||
``` | ||
#### Functions | ||
`function` defines the vim function | ||
``` | ||
function wrong_sum(left, right) do | ||
{:ok, left - right} | ||
end | ||
``` | ||
use it in the editor `:echo WrongSum(1,2)` | ||
|
||
#### Commands | ||
`command` defines the command. | ||
|
||
``` | ||
command just_echo do | ||
NVim.Session.vim_command("echo from remote plugin") | ||
end | ||
``` | ||
use it in the editor `:JustEcho` | ||
|
||
### Session | ||
In the latest example we used `vim_command` method which is part of Neovim remote API. | ||
In the examples below we asume that we import `NVim.Session` in context of plugin. | ||
|
||
### State | ||
Each plugin is [GenServer](http://elixir-lang.org/docs/stable/elixir/GenServer.html). | ||
So you can share the state between all actions while the plugin is running: | ||
```elixir | ||
on_event :cursor_moved do | ||
state = %{state | {move_counts: state[:move_counts] + 1} | ||
end | ||
|
||
command :show_moves_count do | ||
moves_info = "Current moves: #{state[:move_count]}" | ||
vim_command("echo '#{moves_info}'") | ||
end | ||
``` | ||
### Pre-evaluated values | ||
Any vim value can be pre-evaluated before the action will be triggered: | ||
``` | ||
on_event :cursor_hold_i, | ||
pre_evaluate: %{ | ||
"expand('cWORD')" => word_under_cursor | ||
} | ||
do | ||
if word_under_cursor == "Elixir" do | ||
something() | ||
end | ||
end | ||
``` | ||
## Basic scripts | ||
This example demonstrates the highlighting of outdated packeges in your mix.exs | ||
``` | ||
# ~/.config/nvim/rplugin/elixir/scripts/highlight_outdated_packages.exs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
use Mix.Config | ||
|
||
config :logger, | ||
backends: [{LoggerFileBackend, :error_log}], | ||
level: :error | ||
|
||
config :logger, :error_log, | ||
path: Path.expand("#{__DIR__}/../tmp/neovim_elixir_host.log"), | ||
level: :error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
defmodule Mix.Tasks.Nvim.Install do | ||
use Mix.Task | ||
|
||
import Mix.Generator | ||
@shortdoc "Installs the host to provided nvim config path." | ||
|
||
def run(argv) do | ||
{_opts, argv} = OptionParser.parse!(argv) | ||
|
||
case argv do | ||
[] -> | ||
Mix.raise "Expected NVIM_CONFIG_PATH to be given, please use \"mix nvim.install NVIM_CONFIG_PATH\"" | ||
[nvim_config_path | _] -> | ||
create_file Path.join(nvim_config_path, "plugin/elixir_host.vim"), elixir_host_plugin_vim_text, force: true | ||
|
||
remote_plugin_path = Path.join(nvim_config_path, "rplugin/elixir") | ||
|
||
create_directory Path.join(remote_plugin_path, "scripts") | ||
create_directory Path.join(remote_plugin_path, "apps") | ||
create_file Path.join(remote_plugin_path, "mix.exs"), apps_mixfile_text, force: true | ||
create_file Path.join([remote_plugin_path, "config", "config.exs"]), apps_config_text, force: true | ||
|
||
host_path = Path.join([remote_plugin_path, "apps","host"]) | ||
|
||
create_file Path.join(host_path, "mix.exs"), host_mixfile_text, force: true | ||
create_file Path.join([host_path, "config", "config.exs"]), host_config_text, force: true | ||
|
||
print_successful_info | ||
end | ||
end | ||
|
||
defp print_successful_info do | ||
Mix.shell.info [:green, """ | ||
Elixir host succesfully installed. | ||
"""] | ||
end | ||
|
||
embed_text :host_mixfile, ~s""" | ||
defmodule Host.Mixfile do | ||
use Mix.Project | ||
def project do | ||
[app: :host, | ||
version: "#{Mix.Project.config[:version]}", | ||
build_path: "../../_build", | ||
config_path: "../../config/config.exs", | ||
deps_path: "../../deps", | ||
lockfile: "../../mix.lock", | ||
elixir: "~> 1.3", | ||
deps: deps, | ||
aliases: aliases, | ||
escript: escript] | ||
end | ||
def application do | ||
[applications: [:logger, :nvim], env: [plugin_module: NVim.Host.Plugin]] | ||
end | ||
def escript do | ||
[main_module: NVim.Host, emu_args: "-noinput"] | ||
end | ||
defp deps do | ||
[{:nvim, "#{Mix.Project.config[:version]}"}] | ||
end | ||
defp aliases do | ||
["nvim.build_host": ["deps.get", "nvim.build_host"]] | ||
end | ||
end | ||
""" | ||
embed_text :apps_mixfile, """ | ||
defmodule Elixir.Mixfile do | ||
use Mix.Project | ||
def project do | ||
[apps_path: "apps", | ||
deps: []] | ||
end | ||
end | ||
""" | ||
|
||
embed_text :apps_config, """ | ||
use Mix.Config | ||
import_config "../apps/*/config/config.exs" | ||
""" | ||
|
||
embed_text :host_config, ~S""" | ||
use Mix.Config | ||
config :logger, | ||
backends: [{LoggerFileBackend, :error_log}], | ||
level: :error | ||
config :logger, :error_log, | ||
path: Path.expand("#{__DIR__}/../neovim_elixir_host.log"), | ||
level: :error | ||
""" | ||
|
||
embed_text :elixir_host_plugin_vim, """ | ||
let s:nvim_path = expand('<sfile>:p:h:h') | ||
let s:xdg_home_path = expand('<sfile>:p:h:h:h') | ||
function! s:RequireElixirHost(host) | ||
try | ||
let channel_id = rpcstart(s:nvim_path . '/rplugin/elixir/apps/host/host',[]) | ||
if rpcrequest(channel_id, 'poll') == 'ok' | ||
return channel_id | ||
endif | ||
catch | ||
endtry | ||
throw 'Failed to load elixir host.' . expand('<sfile>') . | ||
\ ' More information can be found in elixir host log file.' | ||
endfunction | ||
call remote#host#Register('elixir', '{scripts/*.exs,apps/*}', function('s:RequireElixirHost')) | ||
function! UpdateElixirPlugins() | ||
execute '!cd ' . s:nvim_path . '/rplugin/elixir/apps/host && MIX_ENV=prod mix do deps.get, nvim.build_host --xdg-home-path ' . s:xdg_home_path . ' --vim-rc-path ' . s:nvim_path . '/init.vim' | ||
endfunction | ||
command! UpdateElixirPlugins call UpdateElixirPlugins() | ||
""" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
defmodule Mix.Tasks.Nvim.Remove do | ||
use Mix.Task | ||
|
||
@shortdoc "Removes the host(include ALL installed plugins) for provided nvim config path." | ||
|
||
def run(argv) do | ||
{_opts, argv} = OptionParser.parse!(argv) | ||
|
||
case argv do | ||
[] -> | ||
Mix.raise "Expected NVIM_CONFIG_PATH to be given, please use \"mix nvim.remove NVIM_CONFIG_PATH\"" | ||
[nvim_config_path | _] -> | ||
File.rm Path.join(nvim_config_path, "plugin/elixir_host.vim") | ||
|
||
File.rm Path.join(nvim_config_path, "rplugin/elixir/mix.exs") | ||
safely_remove_directory(Path.join(nvim_config_path, "rplugin/elixir/scripts")) | ||
|
||
File.rm_rf Path.join(nvim_config_path, "rplugin/elixir/_build") | ||
File.rm_rf Path.join(nvim_config_path, "rplugin/elixir/config") | ||
File.rm_rf Path.join(nvim_config_path, "rplugin/elixir/apps/host") | ||
safely_remove_directory(Path.join(nvim_config_path, "rplugin/elixir/apps")) | ||
|
||
Mix.shell.info "Elixir host succesfully removed." | ||
end | ||
end | ||
|
||
defp safely_remove_directory(path) do | ||
if directory_empty?(path) do | ||
File.rm_rf(path) | ||
else | ||
Mix.shell.info "#{path} is not removed because is not empty." | ||
end | ||
end | ||
|
||
defp directory_empty?(path) do | ||
Path.join(path, "*") |> Path.wildcard |> Enum.empty? | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
defmodule NVim.Installer.Mixfile do | ||
use Mix.Project | ||
|
||
def project do | ||
[app: :nvim_installer, | ||
version: "0.1.0", | ||
elixir: "~> 1.3"] | ||
end | ||
|
||
def application do | ||
[applications: []] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
defmodule Mix.Tasks.Nvim.InstallTest do | ||
use ExUnit.Case | ||
|
||
test "installs the host to a provided directory" do | ||
in_tmp_dir("nvim_config", fn(path)-> | ||
output = ExUnit.CaptureIO.capture_io(fn-> Mix.Tasks.Nvim.Install.run [path] end) | ||
|
||
assert_file_exists "#{path}/plugin/elixir_host.vim" | ||
assert_directory_exists "#{path}/rplugin/elixir/scripts" | ||
assert_directory_exists "#{path}/rplugin/elixir/apps/host" | ||
assert_file_exists "#{path}/rplugin/elixir/config/config.exs" | ||
assert_file_exists "#{path}/rplugin/elixir/mix.exs" | ||
|
||
assert output =~ "Elixir host succesfully installed." | ||
end) | ||
end | ||
|
||
defp in_tmp_dir(path, callback) do | ||
expanded_path = Path.expand("../../tmp/#{path}", __DIR__) | ||
File.rm_rf! expanded_path | ||
File.mkdir! expanded_path | ||
|
||
callback.(expanded_path) | ||
end | ||
|
||
defp assert_file_exists(path) do | ||
assert File.exists?(path) | ||
end | ||
|
||
defp assert_directory_exists(path) do | ||
assert File.dir?(path) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ExUnit.start |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
defmodule NVim.Host.Handler do | ||
require Logger | ||
alias NVim.PluginManager | ||
|
||
def on_call(_session, "poll", _params) do | ||
{:ok, "ok"} | ||
end | ||
|
||
def on_call(_session, "specs", [plugin_path]) do | ||
try do | ||
{:ok, plugin} = PluginManager.lookup(plugin_path) | ||
{:ok, plugin.specs} | ||
rescue | ||
any -> | ||
Logger.error("Plugin path: #{plugin_path}, error: #{inspect any}") | ||
{:error, "Troubles with load a plugin. See elixir host log for more information"} | ||
end | ||
end | ||
|
||
def on_call(session, method, params) do | ||
try do | ||
on_action(session, method, params, sync: true) | ||
catch | ||
any -> | ||
Logger.error("cathc error: #{inspect any}") | ||
rescue | ||
error -> | ||
Logger.error("call error: #{inspect error}") | ||
{:error, "Error: #{inspect error}"} | ||
end | ||
end | ||
|
||
def on_notify(session, method, params) do | ||
on_action(session, method, params, sync: false) | ||
end | ||
|
||
defp on_action(_session, method, params, sync: sync) do | ||
[plugin_path, action_type, action_name | rest] = String.split(method, ":") | ||
action_name = if rest != [], do: "#{action_name}:#{hd(rest)}", else: action_name | ||
|
||
case PluginManager.lookup(plugin_path) do | ||
{:ok, plugin} -> | ||
response = plugin.handle_rpc_method(action_type, action_name, params) | ||
if sync, do: handle_plugin_response(response) | ||
_ -> | ||
Logger.error("Plugin #{plugin_path} was not loaded") | ||
{:error, "Problem with loading plugin for: #{method}"} | ||
end | ||
end | ||
|
||
defp handle_plugin_response({status, _value}= response) when status in [:ok, :error], do: response | ||
defp handle_plugin_response(_), do: {:error, "Problem with handle action by plugin"} | ||
end |
Oops, something went wrong.