Skip to content

Commit d9aed96

Browse files
authored
Merge pull request #325 from bfredl/lua
Add lua integration (and update_highlights as an useful example)
2 parents 13f7635 + 2c0880e commit d9aed96

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

neovim/api/buffer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None,
112112
self.request('nvim_buf_clear_highlight', src_id,
113113
line_start, line_end, async_=async_)
114114

115+
def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1,
116+
clear=False, async_=True):
117+
"""Add or update highlights in batch to avoid unnecessary redraws.
118+
119+
A `src_id` must have been allocated prior to use of this function. Use
120+
for instance `nvim.new_highlight_source()` to get a src_id for your
121+
plugin.
122+
123+
`hls` should be a list of highlight items. Each item should be a list
124+
or tuple on the form `("GroupName", linenr, col_start, col_end)` or
125+
`("GroupName", linenr)` to highlight an entire line.
126+
127+
By default existing highlights are preserved. Specify a line range with
128+
clear_start and clear_end to replace highlights in this range. As a
129+
shorthand, use clear=True to clear the entire buffer before adding the
130+
new highlights.
131+
"""
132+
if clear and clear_start is None:
133+
clear_start = 0
134+
lua = self._session._get_lua_private()
135+
lua.update_highlights(self, src_id, hls, clear_start, clear_end,
136+
async_=async_)
137+
115138
@property
116139
def name(self):
117140
"""Get the buffer name."""

neovim/api/nvim.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@
1919

2020
os_chdir = os.chdir
2121

22+
lua_module = """
23+
local a = vim.api
24+
local function update_highlights(buf, src_id, hls, clear_first, clear_end)
25+
if clear_first ~= nil then
26+
a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end)
27+
end
28+
for _,hl in pairs(hls) do
29+
local group, line, col_start, col_end = unpack(hl)
30+
if col_start == nil then
31+
col_start = 0
32+
end
33+
if col_end == nil then
34+
col_end = -1
35+
end
36+
a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end)
37+
end
38+
end
39+
40+
local chid = ...
41+
local mod = {update_highlights=update_highlights}
42+
_G["_pynvim_"..chid] = mod
43+
"""
44+
2245

2346
class Nvim(object):
2447

@@ -92,6 +115,7 @@ def __init__(self, session, channel_id, metadata, types,
92115
self.current = Current(self)
93116
self.session = CompatibilitySession(self)
94117
self.funcs = Funcs(self)
118+
self.lua = LuaFuncs(self)
95119
self.error = NvimError
96120
self._decode = decode
97121
self._err_cb = err_cb
@@ -115,6 +139,12 @@ def _to_nvim(self, obj):
115139
return ExtType(*obj.code_data)
116140
return obj
117141

142+
def _get_lua_private(self):
143+
if not getattr(self._session, "_has_lua", False):
144+
self.exec_lua(lua_module, self.channel_id)
145+
self._session._has_lua = True
146+
return getattr(self.lua, "_pynvim_{}".format(self.channel_id))
147+
118148
def request(self, name, *args, **kwargs):
119149
r"""Send an API request or notification to nvim.
120150
@@ -253,6 +283,27 @@ def call(self, name, *args, **kwargs):
253283
"""Call a vimscript function."""
254284
return self.request('nvim_call_function', name, args, **kwargs)
255285

286+
def exec_lua(self, code, *args, **kwargs):
287+
"""Execute lua code.
288+
289+
Additional parameters are available as `...` inside the lua chunk.
290+
Only statements are executed. To evaluate an expression, prefix it
291+
with `return`: `return my_function(...)`
292+
293+
There is a shorthand syntax to call lua functions with arguments:
294+
295+
nvim.lua.func(1,2)
296+
nvim.lua.mymod.myfunction(data, async_=True)
297+
298+
is equivalent to
299+
300+
nvim.exec_lua("return func(...)", 1, 2)
301+
nvim.exec_lua("mymod.myfunction(...)", data, async_=True)
302+
303+
Note that with `async_=True` there is no return value.
304+
"""
305+
return self.request('nvim_execute_lua', code, args, **kwargs)
306+
256307
def strwidth(self, string):
257308
"""Return the number of display cells `string` occupies.
258309
@@ -467,5 +518,29 @@ def __getattr__(self, name):
467518
return partial(self._nvim.call, name)
468519

469520

521+
class LuaFuncs(object):
522+
523+
"""Wrapper to allow lua functions to be called like python methods."""
524+
525+
def __init__(self, nvim, name=""):
526+
self._nvim = nvim
527+
self.name = name
528+
529+
def __getattr__(self, name):
530+
"""Return wrapper to named api method."""
531+
prefix = self.name + "." if self.name else ""
532+
return LuaFuncs(self._nvim, prefix + name)
533+
534+
def __call__(self, *args, **kwargs):
535+
# first new function after keyword rename, be a bit noisy
536+
if 'async' in kwargs:
537+
raise ValueError('"async" argument is not allowed. '
538+
'Use "async_" instead.')
539+
async_ = kwargs.get('async_', False)
540+
pattern = "return {}(...)" if not async_ else "{}(...)"
541+
code = pattern.format(self.name)
542+
return self._nvim.exec_lua(code, *args, **kwargs)
543+
544+
470545
class NvimError(Exception):
471546
pass

neovim/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def find_module(fullname, path):
4040

4141

4242
def check_async(async_, kwargs, default):
43-
"""Return a value of 'async' in kwargs or default when async_ is None
43+
"""Return a value of 'async' in kwargs or default when async_ is None.
4444
4545
This helper function exists for backward compatibility (See #274).
4646
It shows a warning message when 'async' in kwargs is used to note users.

test/test_buffer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,10 @@ def test_set_items_for_range(vim):
161161
r = vim.current.buffer.range(1, 3)
162162
r[1:3] = ['foo']*3
163163
assert vim.current.buffer[:] == ['a', 'foo', 'foo', 'foo', 'd', 'e']
164+
165+
# NB: we can't easily test the effect of this. But at least run the lua
166+
# function sync, so we know it runs without runtime error with simple args.
167+
def test_update_highlights(vim):
168+
vim.current.buffer[:] = ['a', 'b', 'c']
169+
src_id = vim.new_highlight_source()
170+
vim.current.buffer.update_highlights(src_id, [["Comment", 0, 0, -1], ("String", 1, 0, 1)], clear=True, async_=False)

test/test_vim.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,32 @@ def test_cwd(vim, tmpdir):
168168
cwd_python = vim.command_output('{} print(os.getcwd())'.format(pycmd))
169169
assert cwd_python == cwd_vim
170170
assert cwd_python != cwd_before
171+
172+
lua_code = """
173+
local a = vim.api
174+
local y = ...
175+
function pynvimtest_func(x)
176+
return x+y
177+
end
178+
179+
local function setbuf(buf,lines)
180+
a.nvim_buf_set_lines(buf, 0, -1, true, lines)
181+
end
182+
183+
184+
local function getbuf(buf)
185+
return a.nvim_buf_line_count(buf)
186+
end
187+
188+
pynvimtest = {setbuf=setbuf,getbuf=getbuf}
189+
190+
return "eggspam"
191+
"""
192+
193+
def test_lua(vim):
194+
assert vim.exec_lua(lua_code, 7) == "eggspam"
195+
assert vim.lua.pynvimtest_func(3) == 10
196+
testmod = vim.lua.pynvimtest
197+
buf = vim.current.buffer
198+
testmod.setbuf(buf, ["a", "b", "c", "d"], async_=True)
199+
assert testmod.getbuf(buf) == 4

0 commit comments

Comments
 (0)