Skip to content

Commit

Permalink
init POC version
Browse files Browse the repository at this point in the history
  • Loading branch information
dm1try committed Aug 24, 2016
0 parents commit d22d129
Show file tree
Hide file tree
Showing 29 changed files with 1,386 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
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

106 changes: 106 additions & 0 deletions README.md
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
9 changes: 9 additions & 0 deletions config/config.exs
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
124 changes: 124 additions & 0 deletions installer/lib/install.ex
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
38 changes: 38 additions & 0 deletions installer/lib/remove.ex
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
13 changes: 13 additions & 0 deletions installer/mix.exs
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
33 changes: 33 additions & 0 deletions installer/test/install_test.exs
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
1 change: 1 addition & 0 deletions installer/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start
53 changes: 53 additions & 0 deletions lib/host/handler.ex
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
Loading

0 comments on commit d22d129

Please sign in to comment.