Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
smolck committed Oct 19, 2019
0 parents commit 832b075
Show file tree
Hide file tree
Showing 12 changed files with 1,531 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Files and directories created by pub
.dart_tool/
.packages
pubspec.lock

# Conventional directory for build outputs
build/

# Directory created by dartdoc
doc/api/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version, created by Stagehand
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Dart Nvim API
Neovim API implementation for [Dart](dart.dev). Still a WIP, so
any feedback, contributions, etc. are greatly appreciated.

__NOTE__: Currently, this library
only supports _asynchronous_ calls to Neovim. This is due to how
Dart's [`Socket` class](https://api.dartlang.org/stable/2.5.2/dart-io/Socket-class.html) (used
internally by the `Session` class here), is best used with `async/await`. However,
I do plan to (attempt) creating a blocking implementation, so feel free to open an issue
on the topic if you have any suggestsions or want to help.
8 changes: 8 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Defines a default set of lint rules enforced for
# projects at Google. For details and rationale,
# see https://github.com/dart-lang/pedantic#enabled-lints.
include: package:pedantic/analysis_options.yaml

analyzer:
exclude:
- gen_bindings/neovim.dart
283 changes: 283 additions & 0 deletions gen_bindings/gen_bindings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#!/usr/bin/env python
"""
Dart code generator, based on neovim-lib generator:
https://github.com/daa84/neovim-lib/blob/master/bindings/generate_bindings.py
"""

import msgpack
import sys
import subprocess
import os
import re
import datetime
import string

INPUT = 'bindings_gen'


def make_camel_case(s):
"""
Formats the inputted snake_case string
in camelCase.
"""
new_str = s.replace('_', ' ')
new_str = new_str.split(' ')
first_word = new_str[0]
new_str = string.capwords(' '.join(new_str[1:]))
new_str = first_word + new_str
return new_str.replace(' ', '')


def make_args_from_params(params):
"""
Returns a list which has `.codeData` added to the end
of the names of each element of `params` with a type in
`NeovimTypeVal.EXTTYPES` (`msgpack_dart` can't serialize those types, so it
needs their internal representation, which is `codeData`).
"""
params_clone = params[:]
for val in params_clone:
if val.native_type_arg in NeovimTypeVal.EXTTYPES:
val.name += '.codeData'

return params_clone


def decutf8(inp):
"""
Recursively decode bytes as utf8 into unicode
"""
if isinstance(inp, bytes):
return inp.decode('utf8')
elif isinstance(inp, list):
return [decutf8(x) for x in inp]
elif isinstance(inp, dict):
return {decutf8(key): decutf8(val) for key, val in inp.items()}
else:
return inp


def get_api_info(nvim):
"""
Call the neovim binary to get the api info
"""
args = [nvim, '--api-info']
info = subprocess.check_output(args)
return decutf8(msgpack.unpackb(info))


def generate_file(name, outpath, **kw):
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader(
INPUT), trim_blocks=True)
template = env.get_template(name)
with open(os.path.join(outpath, name), 'w') as fp:
fp.write(template.render(kw))

subprocess.call(["dartfmt", os.path.join(outpath, name), "-w"])
# os.remove(os.path.join(outpath, name + ".bk"))


class UnsupportedType(Exception):
pass


class NeovimTypeVal:
"""
Representation for Neovim Parameter/Return
"""
# msgpack simple types types
SIMPLETYPES_REF = {
'Array': 'List<dynamic>',
'ArrayOf(Integer, 2)': 'List<int>',
'void': 'void',
'Integer': 'int',
'Boolean': 'bool',
'String': 'String',
'Object': 'dynamic',
'Dictionary': 'Map<dynamic, dynamic>',
}

SIMPLETYPES_VAL = {
'Array': 'List<dynamic>',
'ArrayOf(Integer, 2)': 'List<int>',
'void': 'void',
'Integer': 'int',
'Boolean': 'bool',
'String': 'String',
'Object': 'dynamic',
'Dictionary': 'Map<dynamic, dynamic>',
}
# msgpack extension types
EXTTYPES = {
'Window': 'Window',
'Buffer': 'Buffer',
'Tabpage': 'Tabpage',
}
# Unbound Array types
UNBOUND_ARRAY = re.compile('ArrayOf\(\s*(\w+)\s*\)')

def __init__(self, typename, name=''):
self.name = name
self.neovim_type = typename
self.ext = False
self.native_type_arg = NeovimTypeVal.nativeTypeRef(typename)
self.native_type_ret = NeovimTypeVal.nativeTypeVal(typename)

if typename in self.EXTTYPES:
self.ext = True

def __getitem__(self, key):
if key == "native_type_arg":
return self.native_type_arg
if key == "name":
return self.name
return None

@classmethod
def nativeTypeVal(cls, typename):
"""Return the native type for this Neovim type."""
if typename in cls.SIMPLETYPES_VAL:
return cls.SIMPLETYPES_VAL[typename]
elif typename in cls.EXTTYPES:
return cls.EXTTYPES[typename]
elif cls.UNBOUND_ARRAY.match(typename):
m = cls.UNBOUND_ARRAY.match(typename)
return 'List<%s>' % cls.nativeTypeVal(m.groups()[0])
raise UnsupportedType(typename)

@classmethod
def nativeTypeRef(cls, typename):
"""Return the native type for this Neovim type."""
if typename in cls.SIMPLETYPES_REF:
return cls.SIMPLETYPES_REF[typename]
elif typename in cls.EXTTYPES:
return "%s" % cls.EXTTYPES[typename]
elif cls.UNBOUND_ARRAY.match(typename):
m = cls.UNBOUND_ARRAY.match(typename)
return 'List<%s>' % cls.nativeTypeVal(m.groups()[0])
raise UnsupportedType(typename)


class Function:
"""
Representation for a Neovim API Function
"""

def __init__(self, nvim_fun, all_ext_prefixes):
self.valid = False
self.fun = nvim_fun
self.parameters = []
self.name = self.fun['name']
self.since = self.fun['since']

self.ext = self._is_ext(all_ext_prefixes)

try:
self.return_type = NeovimTypeVal(self.fun['return_type'])
if self.ext:
for param in self.fun['parameters'][1:]:
self.parameters.append(NeovimTypeVal(*param))
else:
for param in self.fun['parameters']:
self.parameters.append(NeovimTypeVal(*param))
except UnsupportedType as ex:
print('Found unsupported type(%s) when adding function %s(), skipping' % (
ex, self.name))
return

# Build the argument string - makes it easier for the templates
self.argstring = ', '.join(
['%s %s' % (tv.native_type_arg, tv["name"]) for tv in self.parameters])

# filter function, use only nvim one
# nvim_ui_attach implemented manually
self.valid = self.name.startswith('nvim')\
and self.name != 'nvim_ui_attach'

def _is_ext(self, all_ext_prefixes):
for prefix in all_ext_prefixes:
if self.name.startswith(prefix):
return True
return False


class ExtType:

"""Ext type, Buffer, Window, Tab"""

def __init__(self, typename, info):
self.name = typename
self.id = info['id']
self.prefix = info['prefix']


def print_api(api):
print(api.keys())
for key in api.keys():
if key == 'functions':
print('Functions')
for f in api[key]:
if f['name'].startswith('nvim'):
print(f)
print('')
elif key == 'types':
print('Data Types')
for typ in api[key]:
print('\t%s' % typ)
print('')
elif key == 'error_types':
print('Error Types')
for err, desc in api[key].items():
print('\t%s:%d' % (err, desc['id']))
print('')
elif key == 'version':
print('Version')
print(api[key])
print('')
else:
print('Unknown API info attribute: %s' % key)


if __name__ == '__main__':

if len(sys.argv) < 2 or len(sys.argv) > 3:
print('Usage:')
print('\tgenerate_bindings <nvim>')
print('\tgenerate_bindings <nvim> [path]')
sys.exit(-1)

nvim = sys.argv[1]
outpath = None if len(sys.argv) < 3 else sys.argv[2]

try:
api = get_api_info(sys.argv[1])
except subprocess.CalledProcessError as ex:
print(ex)
sys.exit(-1)

if outpath:
print('Writing auto generated bindings to %s' % outpath)
if not os.path.exists(outpath):
os.makedirs(outpath)
for name in os.listdir(INPUT):
if name.startswith('.'):
continue
if name.endswith('.dart'):
env = {}
env['date'] = datetime.datetime.now()

exttypes = [ExtType(typename, info)
for typename, info in api['types'].items()]
all_ext_prefixes = {exttype.prefix for exttype in exttypes}
functions = [Function(f, all_ext_prefixes)
for f in api['functions']]
env['functions'] = [f for f in functions if f.valid]
env['exttypes'] = exttypes
env['to_camel_case'] = make_camel_case
env['make_args_from_params'] = make_args_from_params
generate_file(name, outpath, **env)

else:
print('Neovim api info:')
print_api(api)
59 changes: 59 additions & 0 deletions gen_bindings/neovim.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Generated {{ date }} by `gen_bindings.py`.
// DO NOT MODIFY DIRECTLY!

import 'dart:async';
import 'package:meta/meta.dart';
import 'package:dart_nvim_api/session.dart';

{% for etype in exttypes %}
class {{ etype.name }} {
dynamic _codeData;
/// Internal value that represents the type.
get codeData => _codeData;
{{ etype.name }}(this._codeData);

{% for f in functions if f.ext and f.name.startswith(etype.prefix) %}
/// since: {{f.since}}
Future<{{ f.return_type.native_type_ret }}> {{ f.name | capitalize | replace(etype.prefix, '') }}(Neovim neovim, {{ f.argstring }}) {
return neovim.session.call("{{f.name}}",
args: [_codeData
{% if f.parameters|count > 0 %}
, {{ f.parameters|map(attribute = "name")|join(", ") }}
{% endif %}
]);
}
{% endfor %}
}

{% endfor %}

class Neovim {
Session _session;
get session => _session;

Neovim.fromSession(this._session);
Neovim.connectToRunningInstance({
@required String host,
@required int port,
}) : _session = Session.fromRunningInstance(host: host, port: port);
Neovim({String nvimBinaryPath})
: _session = Session(nvim: nvimBinaryPath ?? '/usr/bin/nvim');

{% for f in functions if not f.ext %}
{% set trimmedFname = f.name | replace('nvim_', '') %}
Future<{{ f.return_type.native_type_ret }}> {{ to_camel_case(trimmedFname) }}({{ f.argstring }}) async {
var retVal = await _session.call("{{ f.name }}",
args: [{{ make_args_from_params(f.parameters) | map(attribute = "name") | join(", ") }}]);

{% if "Map" in f.return_type.native_type_ret or "List" in f.return_type.native_type_ret %}
retVal = {{ f.return_type.native_type_ret }}.from(retVal);

{% elif f.return_type.native_type_ret != "void" %}
retVal = retVal as {{ f.return_type.native_type_ret }};
{% endif %}

return retVal;
}

{% endfor %}
}
Loading

0 comments on commit 832b075

Please sign in to comment.