commit
5155f7a2a4
|
@ -104,11 +104,20 @@ command.add(nil, {
|
||||||
end, function (text)
|
end, function (text)
|
||||||
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
||||||
end, nil, function(text)
|
end, nil, function(text)
|
||||||
local path_stat, err = system.get_file_info(common.home_expand(text))
|
local filename = common.home_expand(text)
|
||||||
|
local path_stat, err = system.get_file_info(filename)
|
||||||
if err then
|
if err then
|
||||||
core.error("Cannot open file %q: %q", text, err)
|
if err:find("No such file", 1, true) then
|
||||||
|
-- check if the containing directory exists
|
||||||
|
local dirname = common.dirname(filename)
|
||||||
|
local dir_stat = dirname and system.get_file_info(dirname)
|
||||||
|
if not dirname or (dir_stat and dir_stat.type == 'dir') then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
core.error("Cannot open file %s: %s", text, err)
|
||||||
elseif path_stat.type == 'dir' then
|
elseif path_stat.type == 'dir' then
|
||||||
core.error("Cannot open %q, is a folder", text)
|
core.error("Cannot open %s, is a folder", text)
|
||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,7 +43,12 @@ local function append_line_if_last_line(line)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function save(filename)
|
local function save(filename)
|
||||||
doc():save(filename and core.normalize_to_project_dir(filename))
|
local abs_filename
|
||||||
|
if filename then
|
||||||
|
filename = core.normalize_to_project_dir(filename)
|
||||||
|
abs_filename = core.project_absolute_path(filename)
|
||||||
|
end
|
||||||
|
doc():save(filename, abs_filename)
|
||||||
local saved_filename = doc().filename
|
local saved_filename = doc().filename
|
||||||
core.log("Saved \"%s\"", saved_filename)
|
core.log("Saved \"%s\"", saved_filename)
|
||||||
end
|
end
|
||||||
|
@ -363,12 +368,14 @@ local commands = {
|
||||||
end
|
end
|
||||||
core.command_view:set_text(old_filename)
|
core.command_view:set_text(old_filename)
|
||||||
core.command_view:enter("Rename", function(filename)
|
core.command_view:enter("Rename", function(filename)
|
||||||
doc():save(filename)
|
save(common.home_expand(filename))
|
||||||
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
|
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
|
||||||
if filename ~= old_filename then
|
if filename ~= old_filename then
|
||||||
os.remove(old_filename)
|
os.remove(old_filename)
|
||||||
end
|
end
|
||||||
end, common.path_suggest)
|
end, function (text)
|
||||||
|
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
||||||
|
end)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,12 @@ function common.basename(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- can return nil if there is no directory part in the path
|
||||||
|
function common.dirname(path)
|
||||||
|
return path:match("(.+)[\\/][^\\/]+$")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function common.home_encode(text)
|
function common.home_encode(text)
|
||||||
if HOME and string.find(text, HOME, 1, true) == 1 then
|
if HOME and string.find(text, HOME, 1, true) == 1 then
|
||||||
local dir_pos = #HOME + 1
|
local dir_pos = #HOME + 1
|
||||||
|
@ -257,16 +263,6 @@ function common.home_expand(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function common.normalize_path(filename)
|
|
||||||
if PATHSEP == '\\' then
|
|
||||||
filename = filename:gsub('[/\\]', '\\')
|
|
||||||
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
|
|
||||||
return drive and drive:upper() .. rem or filename
|
|
||||||
end
|
|
||||||
return filename
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
local function split_on_slash(s, sep_pattern)
|
local function split_on_slash(s, sep_pattern)
|
||||||
local t = {}
|
local t = {}
|
||||||
if s:match("^[/\\]") then
|
if s:match("^[/\\]") then
|
||||||
|
@ -279,8 +275,27 @@ local function split_on_slash(s, sep_pattern)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function common.normalize_path(filename)
|
||||||
|
if PATHSEP == '\\' then
|
||||||
|
filename = filename:gsub('[/\\]', '\\')
|
||||||
|
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
|
||||||
|
filename = drive and drive:upper() .. rem or filename
|
||||||
|
end
|
||||||
|
local parts = split_on_slash(filename, PATHSEP)
|
||||||
|
local accu = {}
|
||||||
|
for _, part in ipairs(parts) do
|
||||||
|
if part == '..' then
|
||||||
|
table.remove(accu)
|
||||||
|
elseif part ~= '.' then
|
||||||
|
table.insert(accu, part)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(accu, PATHSEP)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function common.path_belongs_to(filename, path)
|
function common.path_belongs_to(filename, path)
|
||||||
return filename and string.find(filename, path .. PATHSEP, 1, true) == 1
|
return string.find(filename, path .. PATHSEP, 1, true) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,17 @@ local function split_lines(text)
|
||||||
return res
|
return res
|
||||||
end
|
end
|
||||||
|
|
||||||
function Doc:new(filename)
|
|
||||||
|
function Doc:new(filename, abs_filename, new_file)
|
||||||
|
self.new_file = new_file
|
||||||
self:reset()
|
self:reset()
|
||||||
if filename then
|
if filename then
|
||||||
|
self:set_filename(filename, abs_filename)
|
||||||
|
if not new_file then
|
||||||
self:load(filename)
|
self:load(filename)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function Doc:reset()
|
function Doc:reset()
|
||||||
|
@ -47,16 +52,15 @@ function Doc:reset_syntax()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function Doc:set_filename(filename)
|
function Doc:set_filename(filename, abs_filename)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.abs_filename = system.absolute_path(filename)
|
self.abs_filename = abs_filename
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function Doc:load(filename)
|
function Doc:load(filename)
|
||||||
local fp = assert( io.open(filename, "rb") )
|
local fp = assert( io.open(filename, "rb") )
|
||||||
self:reset()
|
self:reset()
|
||||||
self:set_filename(filename)
|
|
||||||
self.lines = {}
|
self.lines = {}
|
||||||
for line in fp:lines() do
|
for line in fp:lines() do
|
||||||
if line:byte(-1) == 13 then
|
if line:byte(-1) == 13 then
|
||||||
|
@ -73,17 +77,20 @@ function Doc:load(filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function Doc:save(filename)
|
function Doc:save(filename, abs_filename)
|
||||||
filename = filename or assert(self.filename, "no filename set to default to")
|
if not filename then
|
||||||
|
assert(self.filename, "no filename set to default to")
|
||||||
|
filename = self.filename
|
||||||
|
abs_filename = self.abs_filename
|
||||||
|
end
|
||||||
local fp = assert( io.open(filename, "wb") )
|
local fp = assert( io.open(filename, "wb") )
|
||||||
for _, line in ipairs(self.lines) do
|
for _, line in ipairs(self.lines) do
|
||||||
if self.crlf then line = line:gsub("\n", "\r\n") end
|
if self.crlf then line = line:gsub("\n", "\r\n") end
|
||||||
fp:write(line)
|
fp:write(line)
|
||||||
end
|
end
|
||||||
fp:close()
|
fp:close()
|
||||||
if filename then
|
self:set_filename(filename, abs_filename)
|
||||||
self:set_filename(filename)
|
self.new_file = false
|
||||||
end
|
|
||||||
self:reset_syntax()
|
self:reset_syntax()
|
||||||
self:clean()
|
self:clean()
|
||||||
end
|
end
|
||||||
|
@ -95,7 +102,7 @@ end
|
||||||
|
|
||||||
|
|
||||||
function Doc:is_dirty()
|
function Doc:is_dirty()
|
||||||
return self.clean_change_id ~= self:get_change_id()
|
return self.clean_change_id ~= self:get_change_id() or self.new_file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -342,11 +342,11 @@ local style = require "core.style"
|
||||||
|
|
||||||
-- enable or disable plugin loading setting config entries:
|
-- enable or disable plugin loading setting config entries:
|
||||||
|
|
||||||
-- enable trimwhitespace, otherwise it is disable by default:
|
-- enable plugins.trimwhitespace, otherwise it is disable by default:
|
||||||
-- config.trimwhitespace = true
|
-- config.plugins.trimwhitespace = true
|
||||||
--
|
--
|
||||||
-- disable detectindent, otherwise it is enabled by default
|
-- disable detectindent, otherwise it is enabled by default
|
||||||
-- config.detectindent = false
|
-- config.plugins.detectindent = false
|
||||||
]])
|
]])
|
||||||
init_file:close()
|
init_file:close()
|
||||||
end
|
end
|
||||||
|
@ -620,24 +620,6 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- DEPRECATED function
|
|
||||||
core.doc_save_hooks = {}
|
|
||||||
function core.add_save_hook(fn)
|
|
||||||
core.error("The function core.add_save_hook is deprecated." ..
|
|
||||||
" Modules should now directly override the Doc:save function.")
|
|
||||||
core.doc_save_hooks[#core.doc_save_hooks + 1] = fn
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- DEPRECATED function
|
|
||||||
function core.on_doc_save(filename)
|
|
||||||
-- for backward compatibility in modules. Hooks are deprecated, the function Doc:save
|
|
||||||
-- should be directly overidded.
|
|
||||||
for _, hook in ipairs(core.doc_save_hooks) do
|
|
||||||
hook(filename)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function quit_with_function(quit_fn, force)
|
local function quit_with_function(quit_fn, force)
|
||||||
if force then
|
if force then
|
||||||
delete_temp_files()
|
delete_temp_files()
|
||||||
|
@ -695,28 +677,30 @@ function core.load_plugins()
|
||||||
userdir = {dir = USERDIR, plugins = {}},
|
userdir = {dir = USERDIR, plugins = {}},
|
||||||
datadir = {dir = DATADIR, plugins = {}},
|
datadir = {dir = DATADIR, plugins = {}},
|
||||||
}
|
}
|
||||||
for _, root_dir in ipairs {USERDIR, DATADIR} do
|
local files = {}
|
||||||
|
for _, root_dir in ipairs {DATADIR, USERDIR} do
|
||||||
local plugin_dir = root_dir .. "/plugins"
|
local plugin_dir = root_dir .. "/plugins"
|
||||||
local files = system.list_dir(plugin_dir)
|
for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
|
||||||
for _, filename in ipairs(files or {}) do
|
files[filename] = plugin_dir -- user plugins will always replace system plugins
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for filename, plugin_dir in pairs(files) do
|
||||||
local basename = filename:match("(.-)%.lua$") or filename
|
local basename = filename:match("(.-)%.lua$") or filename
|
||||||
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
|
local version_match = check_plugin_version(plugin_dir .. '/' .. filename)
|
||||||
if is_lua_file then
|
|
||||||
if not version_match then
|
if not version_match then
|
||||||
core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir)
|
core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir)
|
||||||
local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins
|
local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins
|
||||||
ls[#ls + 1] = filename
|
table.insert(list, filename)
|
||||||
elseif config.plugins[basename] ~= false then
|
end
|
||||||
local modname = "plugins." .. basename
|
if version_match and config.plugins[basename] ~= false then
|
||||||
local ok = core.try(require, modname)
|
local ok = core.try(require, "plugins." .. basename)
|
||||||
if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end
|
if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end
|
||||||
if not ok then
|
if not ok then
|
||||||
no_errors = false
|
no_errors = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
return no_errors, refused_list
|
return no_errors, refused_list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -810,10 +794,30 @@ function core.normalize_to_project_dir(filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The function below works like system.absolute_path except it
|
||||||
|
-- doesn't fail if the file does not exist. We consider that the
|
||||||
|
-- current dir is core.project_dir so relative filename are considered
|
||||||
|
-- to be in core.project_dir.
|
||||||
|
-- Please note that .. or . in the filename are not taken into account.
|
||||||
|
-- This function should get only filenames normalized using
|
||||||
|
-- common.normalize_path function.
|
||||||
|
function core.project_absolute_path(filename)
|
||||||
|
if filename:match('^%a:\\') or filename:find('/', 1, true) then
|
||||||
|
return filename
|
||||||
|
else
|
||||||
|
return core.project_dir .. PATHSEP .. filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function core.open_doc(filename)
|
function core.open_doc(filename)
|
||||||
|
local new_file = not filename or not system.get_file_info(filename)
|
||||||
|
local abs_filename
|
||||||
if filename then
|
if filename then
|
||||||
|
-- normalize filename and set absolute filename then
|
||||||
-- try to find existing doc for filename
|
-- try to find existing doc for filename
|
||||||
local abs_filename = system.absolute_path(filename)
|
filename = core.normalize_to_project_dir(filename)
|
||||||
|
abs_filename = core.project_absolute_path(filename)
|
||||||
for _, doc in ipairs(core.docs) do
|
for _, doc in ipairs(core.docs) do
|
||||||
if doc.abs_filename and abs_filename == doc.abs_filename then
|
if doc.abs_filename and abs_filename == doc.abs_filename then
|
||||||
return doc
|
return doc
|
||||||
|
@ -821,8 +825,7 @@ function core.open_doc(filename)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- no existing doc for filename; create new
|
-- no existing doc for filename; create new
|
||||||
filename = filename and core.normalize_to_project_dir(filename)
|
local doc = Doc(filename, abs_filename, new_file)
|
||||||
local doc = Doc(filename)
|
|
||||||
table.insert(core.docs, doc)
|
table.insert(core.docs, doc)
|
||||||
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename)
|
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename)
|
||||||
return doc
|
return doc
|
||||||
|
|
|
@ -108,6 +108,7 @@ keymap.add_direct {
|
||||||
["ctrl+shift+c"] = "core:change-project-folder",
|
["ctrl+shift+c"] = "core:change-project-folder",
|
||||||
["ctrl+shift+o"] = "core:open-project-folder",
|
["ctrl+shift+o"] = "core:open-project-folder",
|
||||||
["alt+return"] = "core:toggle-fullscreen",
|
["alt+return"] = "core:toggle-fullscreen",
|
||||||
|
["f11"] = "core:toggle-fullscreen",
|
||||||
|
|
||||||
["alt+shift+j"] = "root:split-left",
|
["alt+shift+j"] = "root:split-left",
|
||||||
["alt+shift+l"] = "root:split-right",
|
["alt+shift+l"] = "root:split-right",
|
||||||
|
|
|
@ -8,25 +8,65 @@ local keymap = require "core.keymap"
|
||||||
local translate = require "core.doc.translate"
|
local translate = require "core.doc.translate"
|
||||||
local RootView = require "core.rootview"
|
local RootView = require "core.rootview"
|
||||||
local DocView = require "core.docview"
|
local DocView = require "core.docview"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
|
||||||
config.plugins.autocomplete = { max_suggestions = 6 }
|
config.plugins.autocomplete = {
|
||||||
|
-- Amount of characters that need to be written for autocomplete
|
||||||
|
min_len = 1
|
||||||
|
-- The max amount of visible items
|
||||||
|
max_height = 6
|
||||||
|
-- The max amount of scrollable items
|
||||||
|
max_suggestions = 100
|
||||||
|
}
|
||||||
|
|
||||||
local autocomplete = {}
|
local autocomplete = {}
|
||||||
autocomplete.map = {}
|
|
||||||
|
|
||||||
|
autocomplete.map = {}
|
||||||
|
autocomplete.map_manually = {}
|
||||||
|
autocomplete.on_close = nil
|
||||||
|
|
||||||
|
-- Flag that indicates if the autocomplete box was manually triggered
|
||||||
|
-- with the autocomplete.complete() function to prevent the suggestions
|
||||||
|
-- from getting cluttered with arbitrary document symbols by using the
|
||||||
|
-- autocomplete.map_manually table.
|
||||||
|
local triggered_manually = false
|
||||||
|
|
||||||
local mt = { __tostring = function(t) return t.text end }
|
local mt = { __tostring = function(t) return t.text end }
|
||||||
|
|
||||||
function autocomplete.add(t)
|
function autocomplete.add(t, triggered_manually)
|
||||||
local items = {}
|
local items = {}
|
||||||
for text, info in pairs(t.items) do
|
for text, info in pairs(t.items) do
|
||||||
|
if type(info) == "table" then
|
||||||
|
table.insert(
|
||||||
|
items,
|
||||||
|
setmetatable(
|
||||||
|
{
|
||||||
|
text = text,
|
||||||
|
info = info.info,
|
||||||
|
desc = info.desc, -- Description shown on item selected
|
||||||
|
cb = info.cb, -- A callback called once when item is selected
|
||||||
|
data = info.data -- Optional data that can be used on cb
|
||||||
|
},
|
||||||
|
mt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
info = (type(info) == "string") and info
|
info = (type(info) == "string") and info
|
||||||
table.insert(items, setmetatable({ text = text, info = info }, mt))
|
table.insert(items, setmetatable({ text = text, info = info }, mt))
|
||||||
end
|
end
|
||||||
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local max_symbols = config.max_symbols or 2000
|
if not triggered_manually then
|
||||||
|
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
|
||||||
|
else
|
||||||
|
autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Thread that scans open document symbols and cache them
|
||||||
|
--
|
||||||
|
local max_symbols = config.max_symbols
|
||||||
|
|
||||||
core.add_thread(function()
|
core.add_thread(function()
|
||||||
local cache = setmetatable({}, { __mode = "k" })
|
local cache = setmetatable({}, { __mode = "k" })
|
||||||
|
@ -109,16 +149,39 @@ local last_line, last_col
|
||||||
local function reset_suggestions()
|
local function reset_suggestions()
|
||||||
suggestions_idx = 1
|
suggestions_idx = 1
|
||||||
suggestions = {}
|
suggestions = {}
|
||||||
|
|
||||||
|
triggered_manually = false
|
||||||
|
|
||||||
|
local doc = core.active_view.doc
|
||||||
|
if autocomplete.on_close then
|
||||||
|
autocomplete.on_close(doc, suggestions[suggestions_idx])
|
||||||
|
autocomplete.on_close = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function in_table(value, table_array)
|
||||||
|
for i, element in pairs(table_array) do
|
||||||
|
if element == value then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local function update_suggestions()
|
local function update_suggestions()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
local filename = doc and doc.filename or ""
|
local filename = doc and doc.filename or ""
|
||||||
|
|
||||||
|
local map = autocomplete.map
|
||||||
|
|
||||||
|
if triggered_manually then
|
||||||
|
map = autocomplete.map_manually
|
||||||
|
end
|
||||||
|
|
||||||
-- get all relevant suggestions for given filename
|
-- get all relevant suggestions for given filename
|
||||||
local items = {}
|
local items = {}
|
||||||
for _, v in pairs(autocomplete.map) do
|
for _, v in pairs(map) do
|
||||||
if common.match_pattern(filename, v.files) then
|
if common.match_pattern(filename, v.files) then
|
||||||
for _, item in pairs(v.items) do
|
for _, item in pairs(v.items) do
|
||||||
table.insert(items, item)
|
table.insert(items, item)
|
||||||
|
@ -138,7 +201,6 @@ local function update_suggestions()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_partial_symbol()
|
local function get_partial_symbol()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
local line2, col2 = doc:get_selection()
|
local line2, col2 = doc:get_selection()
|
||||||
|
@ -146,14 +208,12 @@ local function get_partial_symbol()
|
||||||
return doc:get_text(line1, col1, line2, col2)
|
return doc:get_text(line1, col1, line2, col2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_active_view()
|
local function get_active_view()
|
||||||
if getmetatable(core.active_view) == DocView then
|
if getmetatable(core.active_view) == DocView then
|
||||||
return core.active_view
|
return core.active_view
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_suggestions_rect(av)
|
local function get_suggestions_rect(av)
|
||||||
if #suggestions == 0 then
|
if #suggestions == 0 then
|
||||||
return 0, 0, 0, 0
|
return 0, 0, 0, 0
|
||||||
|
@ -175,15 +235,67 @@ local function get_suggestions_rect(av)
|
||||||
max_width = math.max(max_width, w)
|
max_width = math.max(max_width, w)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local ah = config.plugins.autocomplete.max_height
|
||||||
|
|
||||||
|
local max_items = #suggestions
|
||||||
|
if max_items > ah then
|
||||||
|
max_items = ah
|
||||||
|
end
|
||||||
|
|
||||||
|
-- additional line to display total items
|
||||||
|
max_items = max_items + 1
|
||||||
|
|
||||||
|
if max_width < 150 then
|
||||||
|
max_width = 150
|
||||||
|
end
|
||||||
|
|
||||||
return
|
return
|
||||||
x - style.padding.x,
|
x - style.padding.x,
|
||||||
y - style.padding.y,
|
y - style.padding.y,
|
||||||
max_width + style.padding.x * 2,
|
max_width + style.padding.x * 2,
|
||||||
#suggestions * (th + style.padding.y) + style.padding.y
|
max_items * (th + style.padding.y) + style.padding.y
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function draw_description_box(text, av, sx, sy, sw, sh)
|
||||||
|
local width = 0
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for line in string.gmatch(text.."\n", "(.-)\n") do
|
||||||
|
width = math.max(width, style.font:get_width(line))
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local height = #lines * style.font:get_height()
|
||||||
|
|
||||||
|
-- draw background rect
|
||||||
|
renderer.draw_rect(
|
||||||
|
sx + sw + style.padding.x / 4,
|
||||||
|
sy,
|
||||||
|
width + style.padding.x * 2,
|
||||||
|
height + style.padding.y * 2,
|
||||||
|
style.background3
|
||||||
|
)
|
||||||
|
|
||||||
|
-- draw text
|
||||||
|
local lh = style.font:get_height()
|
||||||
|
local y = sy + style.padding.y
|
||||||
|
local x = sx + sw + style.padding.x / 4
|
||||||
|
|
||||||
|
for _, line in pairs(lines) do
|
||||||
|
common.draw_text(
|
||||||
|
style.font, style.text, line, "left", x + style.padding.x, y, width, lh
|
||||||
|
)
|
||||||
|
y = y + lh
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function draw_suggestions_box(av)
|
local function draw_suggestions_box(av)
|
||||||
|
if #suggestions <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ah = config.plugins.autocomplete.max_height
|
||||||
|
|
||||||
-- draw background rect
|
-- draw background rect
|
||||||
local rx, ry, rw, rh = get_suggestions_rect(av)
|
local rx, ry, rw, rh = get_suggestions_rect(av)
|
||||||
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
||||||
|
@ -192,7 +304,14 @@ local function draw_suggestions_box(av)
|
||||||
local font = av:get_font()
|
local font = av:get_font()
|
||||||
local lh = font:get_height() + style.padding.y
|
local lh = font:get_height() + style.padding.y
|
||||||
local y = ry + style.padding.y / 2
|
local y = ry + style.padding.y / 2
|
||||||
for i, s in ipairs(suggestions) do
|
local show_count = #suggestions <= ah and #suggestions or ah
|
||||||
|
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
|
||||||
|
|
||||||
|
for i=start_index, start_index+show_count-1, 1 do
|
||||||
|
if not suggestions[i] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
local s = suggestions[i]
|
||||||
local color = (i == suggestions_idx) and style.accent or style.text
|
local color = (i == suggestions_idx) and style.accent or style.text
|
||||||
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
|
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
|
||||||
if s.info then
|
if s.info then
|
||||||
|
@ -200,26 +319,55 @@ local function draw_suggestions_box(av)
|
||||||
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
|
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
|
||||||
end
|
end
|
||||||
y = y + lh
|
y = y + lh
|
||||||
|
if suggestions_idx == i then
|
||||||
|
if s.cb then
|
||||||
|
s.cb(suggestions_idx, s)
|
||||||
|
s.cb = nil
|
||||||
|
s.data = nil
|
||||||
|
end
|
||||||
|
if s.desc and #s.desc > 0 then
|
||||||
|
draw_description_box(s.desc, av, rx, ry, rw, rh)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
renderer.draw_rect(rx, y, rw, 2, style.caret)
|
||||||
|
renderer.draw_rect(rx, y+2, rw, lh, style.background)
|
||||||
|
common.draw_text(
|
||||||
|
style.font,
|
||||||
|
style.accent,
|
||||||
|
"Items",
|
||||||
|
"left",
|
||||||
|
rx + style.padding.x, y, rw, lh
|
||||||
|
)
|
||||||
|
common.draw_text(
|
||||||
|
style.font,
|
||||||
|
style.accent,
|
||||||
|
tostring(suggestions_idx) .. "/" .. tostring(#suggestions),
|
||||||
|
"right",
|
||||||
|
rx, y, rw - style.padding.x, lh
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
-- patch event logic into RootView
|
local function show_autocomplete()
|
||||||
local on_text_input = RootView.on_text_input
|
|
||||||
local update = RootView.update
|
|
||||||
local draw = RootView.draw
|
|
||||||
|
|
||||||
|
|
||||||
RootView.on_text_input = function(...)
|
|
||||||
on_text_input(...)
|
|
||||||
|
|
||||||
local av = get_active_view()
|
local av = get_active_view()
|
||||||
if av then
|
if av then
|
||||||
-- update partial symbol and suggestions
|
-- update partial symbol and suggestions
|
||||||
partial = get_partial_symbol()
|
partial = get_partial_symbol()
|
||||||
if #partial >= 3 then
|
|
||||||
|
if #partial >= config.plugins.autocomplete.min_len or triggered_manually then
|
||||||
update_suggestions()
|
update_suggestions()
|
||||||
|
|
||||||
|
if not triggered_manually then
|
||||||
last_line, last_col = av.doc:get_selection()
|
last_line, last_col = av.doc:get_selection()
|
||||||
|
else
|
||||||
|
local line, col = av.doc:get_selection()
|
||||||
|
local char = av.doc:get_char(line, col-1, line, col-1)
|
||||||
|
|
||||||
|
if char:match("%s") or (char:match("%p") and col ~= last_col) then
|
||||||
|
reset_suggestions()
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
reset_suggestions()
|
reset_suggestions()
|
||||||
end
|
end
|
||||||
|
@ -233,6 +381,30 @@ RootView.on_text_input = function(...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Patch event logic into RootView and Doc
|
||||||
|
--
|
||||||
|
local on_text_input = RootView.on_text_input
|
||||||
|
local on_text_remove = Doc.remove
|
||||||
|
local update = RootView.update
|
||||||
|
local draw = RootView.draw
|
||||||
|
|
||||||
|
RootView.on_text_input = function(...)
|
||||||
|
on_text_input(...)
|
||||||
|
show_autocomplete()
|
||||||
|
end
|
||||||
|
|
||||||
|
Doc.remove = function(self, line1, col1, line2, col2)
|
||||||
|
on_text_remove(self, line1, col1, line2, col2)
|
||||||
|
|
||||||
|
if triggered_manually and line1 == line2 then
|
||||||
|
if last_col >= col1 then
|
||||||
|
reset_suggestions()
|
||||||
|
else
|
||||||
|
show_autocomplete()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
RootView.update = function(...)
|
RootView.update = function(...)
|
||||||
update(...)
|
update(...)
|
||||||
|
@ -241,12 +413,18 @@ RootView.update = function(...)
|
||||||
if av then
|
if av then
|
||||||
-- reset suggestions if caret was moved
|
-- reset suggestions if caret was moved
|
||||||
local line, col = av.doc:get_selection()
|
local line, col = av.doc:get_selection()
|
||||||
|
|
||||||
|
if not triggered_manually then
|
||||||
if line ~= last_line or col ~= last_col then
|
if line ~= last_line or col ~= last_col then
|
||||||
reset_suggestions()
|
reset_suggestions()
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
if line ~= last_line or col < last_col then
|
||||||
|
reset_suggestions()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
RootView.draw = function(...)
|
RootView.draw = function(...)
|
||||||
draw(...)
|
draw(...)
|
||||||
|
@ -258,12 +436,53 @@ RootView.draw = function(...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Public functions
|
||||||
|
--
|
||||||
|
function autocomplete.open(on_close)
|
||||||
|
triggered_manually = true
|
||||||
|
|
||||||
|
if on_close then
|
||||||
|
autocomplete.on_close = on_close
|
||||||
|
end
|
||||||
|
|
||||||
|
local av = get_active_view()
|
||||||
|
last_line, last_col = av.doc:get_selection()
|
||||||
|
update_suggestions()
|
||||||
|
end
|
||||||
|
|
||||||
|
function autocomplete.close()
|
||||||
|
reset_suggestions()
|
||||||
|
end
|
||||||
|
|
||||||
|
function autocomplete.is_open()
|
||||||
|
return #suggestions > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function autocomplete.complete(completions, on_close)
|
||||||
|
reset_suggestions()
|
||||||
|
|
||||||
|
autocomplete.map_manually = {}
|
||||||
|
autocomplete.add(completions, true)
|
||||||
|
|
||||||
|
autocomplete.open(on_close)
|
||||||
|
end
|
||||||
|
|
||||||
|
function autocomplete.can_complete()
|
||||||
|
if #partial >= config.plugins.autocomplete.min_len then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Commands
|
||||||
|
--
|
||||||
local function predicate()
|
local function predicate()
|
||||||
return get_active_view() and #suggestions > 0
|
return get_active_view() and #suggestions > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
command.add(predicate, {
|
command.add(predicate, {
|
||||||
["autocomplete:complete"] = function()
|
["autocomplete:complete"] = function()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
|
@ -288,7 +507,9 @@ command.add(predicate, {
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Keymaps
|
||||||
|
--
|
||||||
keymap.add {
|
keymap.add {
|
||||||
["tab"] = "autocomplete:complete",
|
["tab"] = "autocomplete:complete",
|
||||||
["up"] = "autocomplete:previous",
|
["up"] = "autocomplete:previous",
|
||||||
|
|
|
@ -55,6 +55,17 @@ syntax.add {
|
||||||
["true"] = "literal",
|
["true"] = "literal",
|
||||||
["false"] = "literal",
|
["false"] = "literal",
|
||||||
["NULL"] = "literal",
|
["NULL"] = "literal",
|
||||||
|
["#include"] = "keyword",
|
||||||
|
["#if"] = "keyword",
|
||||||
|
["#ifdef"] = "keyword",
|
||||||
|
["#ifndef"] = "keyword",
|
||||||
|
["#else"] = "keyword",
|
||||||
|
["#elseif"] = "keyword",
|
||||||
|
["#endif"] = "keyword",
|
||||||
|
["#define"] = "keyword",
|
||||||
|
["#warning"] = "keyword",
|
||||||
|
["#error"] = "keyword",
|
||||||
|
["#pragma"] = "keyword",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,28 +10,166 @@
|
||||||
#include <reproc/reproc.h>
|
#include <reproc/reproc.h>
|
||||||
#include "api.h"
|
#include "api.h"
|
||||||
|
|
||||||
#define READ_BUF_SIZE 4096
|
#define READ_BUF_SIZE 2048
|
||||||
|
|
||||||
|
#define L_GETTABLE(L, idx, key, conv, def) ( \
|
||||||
|
lua_getfield(L, idx, key), \
|
||||||
|
conv(L, -1, def) \
|
||||||
|
)
|
||||||
|
|
||||||
|
#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def)
|
||||||
|
#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def)
|
||||||
|
|
||||||
|
#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))
|
||||||
|
|
||||||
|
#define L_RETURN_REPROC_ERROR(L, code) { \
|
||||||
|
lua_pushnil(L); \
|
||||||
|
lua_pushstring(L, reproc_strerror(code)); \
|
||||||
|
lua_pushnumber(L, code); \
|
||||||
|
return 3; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ASSERT_MALLOC(ptr) \
|
||||||
|
if (ptr == NULL) \
|
||||||
|
L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM)
|
||||||
|
|
||||||
|
#define ASSERT_REPROC_ERRNO(L, code) { \
|
||||||
|
if (code < 0) \
|
||||||
|
L_RETURN_REPROC_ERROR(L, code) \
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
reproc_t * process;
|
reproc_t * process;
|
||||||
lua_State* L;
|
bool running;
|
||||||
|
int returncode;
|
||||||
} process_t;
|
} process_t;
|
||||||
|
|
||||||
static int process_new(lua_State* L)
|
// this function should be called instead of reproc_wait
|
||||||
|
static int poll_process(process_t* proc, int timeout)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_newuserdata(
|
int ret = reproc_wait(proc->process, timeout);
|
||||||
L, sizeof(process_t)
|
if (ret != REPROC_ETIMEDOUT) {
|
||||||
|
proc->running = false;
|
||||||
|
proc->returncode = ret;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int kill_process(process_t* proc)
|
||||||
|
{
|
||||||
|
int ret = reproc_stop(
|
||||||
|
proc->process,
|
||||||
|
(reproc_stop_actions) {
|
||||||
|
{REPROC_STOP_KILL, 0},
|
||||||
|
{REPROC_STOP_TERMINATE, 0},
|
||||||
|
{REPROC_STOP_NOOP, 0}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
memset(self, 0, sizeof(process_t));
|
if (ret != REPROC_ETIMEDOUT) {
|
||||||
|
proc->running = false;
|
||||||
|
proc->returncode = ret;
|
||||||
|
}
|
||||||
|
|
||||||
self->process = NULL;
|
return ret;
|
||||||
self->L = L;
|
}
|
||||||
|
|
||||||
luaL_getmetatable(L, API_TYPE_PROCESS);
|
static int process_start(lua_State* L)
|
||||||
lua_setmetatable(L, -2);
|
{
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
if (lua_isnoneornil(L, 2)) {
|
||||||
|
lua_settop(L, 1); // remove the nil if it's there
|
||||||
|
lua_newtable(L);
|
||||||
|
}
|
||||||
|
luaL_checktype(L, 2, LUA_TTABLE);
|
||||||
|
|
||||||
|
int cmd_len = lua_rawlen(L, 1);
|
||||||
|
const char** cmd = malloc(sizeof(char *) * (cmd_len + 1));
|
||||||
|
ASSERT_MALLOC(cmd);
|
||||||
|
cmd[cmd_len] = NULL;
|
||||||
|
|
||||||
|
for(int i = 0; i < cmd_len; i++) {
|
||||||
|
lua_rawgeti(L, 1, i + 1);
|
||||||
|
|
||||||
|
cmd[i] = luaL_checkstring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int deadline = L_GETNUM(L, 2, "timeout", 0);
|
||||||
|
const char* cwd =L_GETSTR(L, 2, "cwd", NULL);
|
||||||
|
int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT);
|
||||||
|
int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT);
|
||||||
|
int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT);
|
||||||
|
lua_pop(L, 5); // remove args we just read
|
||||||
|
|
||||||
|
if (
|
||||||
|
redirect_in > REPROC_REDIRECT_STDOUT
|
||||||
|
|| redirect_out > REPROC_REDIRECT_STDOUT
|
||||||
|
|| redirect_err > REPROC_REDIRECT_STDOUT)
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// env
|
||||||
|
luaL_getsubtable(L, 2, "env");
|
||||||
|
const char **env = NULL;
|
||||||
|
int env_len = 0;
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
env_len++;
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env_len > 0) {
|
||||||
|
env = malloc(sizeof(char*) * (env_len + 1));
|
||||||
|
env[env_len] = NULL;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
lua_pushliteral(L, "=");
|
||||||
|
lua_pushvalue(L, -3); // push the key to the top
|
||||||
|
lua_concat(L, 3); // key=value
|
||||||
|
|
||||||
|
env[i++] = luaL_checkstring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reproc_t* proc = reproc_new();
|
||||||
|
int out = reproc_start(
|
||||||
|
proc,
|
||||||
|
(const char* const*) cmd,
|
||||||
|
(reproc_options) {
|
||||||
|
.working_directory = cwd,
|
||||||
|
.deadline = deadline,
|
||||||
|
.nonblocking = true,
|
||||||
|
.env = {
|
||||||
|
.behavior = REPROC_ENV_EXTEND,
|
||||||
|
.extra = env
|
||||||
|
},
|
||||||
|
.redirect = {
|
||||||
|
.in.type = redirect_in,
|
||||||
|
.out.type = redirect_out,
|
||||||
|
.err.type = redirect_err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (out < 0) {
|
||||||
|
reproc_destroy(proc);
|
||||||
|
L_RETURN_REPROC_ERROR(L, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t* self = lua_newuserdata(L, sizeof(process_t));
|
||||||
|
self->process = proc;
|
||||||
|
self->running = true;
|
||||||
|
|
||||||
|
// this is equivalent to using lua_setmetatable()
|
||||||
|
luaL_setmetatable(L, API_TYPE_PROCESS);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,24 +177,20 @@ static int process_strerror(lua_State* L)
|
||||||
{
|
{
|
||||||
int error_code = luaL_checknumber(L, 1);
|
int error_code = luaL_checknumber(L, 1);
|
||||||
|
|
||||||
if(error_code){
|
if (error_code < 0)
|
||||||
lua_pushstring(
|
lua_pushstring(L, reproc_strerror(error_code));
|
||||||
L,
|
else
|
||||||
reproc_strerror(error_code)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_gc(lua_State* L)
|
static int f_gc(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process) {
|
if(self->process) {
|
||||||
reproc_kill(self->process);
|
kill_process(self);
|
||||||
reproc_destroy(self->process);
|
reproc_destroy(self->process);
|
||||||
self->process = NULL;
|
self->process = NULL;
|
||||||
}
|
}
|
||||||
|
@ -64,330 +198,211 @@ static int process_gc(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_start(lua_State* L)
|
static int f_tostring(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
luaL_checktype(L, 2, LUA_TTABLE);
|
|
||||||
|
|
||||||
char* path = NULL;
|
|
||||||
size_t path_len = 0;
|
|
||||||
|
|
||||||
if(lua_type(L, 3) == LUA_TSTRING){
|
|
||||||
path = (char*) lua_tolstring(L, 3, &path_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t deadline = 0;
|
|
||||||
|
|
||||||
if(lua_type(L, 4) == LUA_TNUMBER){
|
|
||||||
deadline = lua_tonumber(L, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t table_len = luaL_len(L, 2);
|
|
||||||
char* command[table_len+1];
|
|
||||||
command[table_len] = NULL;
|
|
||||||
|
|
||||||
int i;
|
|
||||||
for(i=1; i<=table_len; i++){
|
|
||||||
lua_pushnumber(L, i);
|
|
||||||
lua_gettable(L, 2);
|
|
||||||
|
|
||||||
command[i-1] = (char*) lua_tostring(L, -1);
|
|
||||||
|
|
||||||
lua_remove(L, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(self->process){
|
|
||||||
reproc_kill(self->process);
|
|
||||||
reproc_destroy(self->process);
|
|
||||||
}
|
|
||||||
|
|
||||||
self->process = reproc_new();
|
|
||||||
|
|
||||||
int out = reproc_start(
|
|
||||||
self->process,
|
|
||||||
(const char* const*) command,
|
|
||||||
(reproc_options){
|
|
||||||
.working_directory = path,
|
|
||||||
.deadline = deadline,
|
|
||||||
.nonblocking=true,
|
|
||||||
.redirect.err.type=REPROC_REDIRECT_PIPE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if(out > 0) {
|
|
||||||
lua_pushboolean(L, 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reproc_destroy(self->process);
|
|
||||||
self->process = NULL;
|
|
||||||
lua_pushnumber(L, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
lua_pushliteral(L, API_TYPE_PROCESS);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_pid(lua_State* L)
|
static int f_pid(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
lua_pushnumber(L, reproc_pid(self->process));
|
||||||
int id = reproc_pid(self->process);
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if(id > 0){
|
static int f_returncode(lua_State *L)
|
||||||
lua_pushnumber(L, id);
|
{
|
||||||
} else {
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
lua_pushnumber(L, 0);
|
int ret = poll_process(self, 0);
|
||||||
}
|
|
||||||
} else {
|
if (self->running)
|
||||||
lua_pushnumber(L, 0);
|
lua_pushnil(L);
|
||||||
}
|
else
|
||||||
|
lua_pushnumber(L, ret);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int g_read(lua_State* L, int stream)
|
static int g_read(lua_State* L, int stream)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE);
|
||||||
|
|
||||||
if(self->process){
|
luaL_Buffer b;
|
||||||
int read_size = READ_BUF_SIZE;
|
uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size);
|
||||||
if (lua_type(L, 2) == LUA_TNUMBER){
|
|
||||||
read_size = (int) lua_tonumber(L, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
int tries = 1;
|
int out = reproc_read(
|
||||||
if (lua_type(L, 3) == LUA_TNUMBER){
|
|
||||||
tries = (int) lua_tonumber(L, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
int out = 0;
|
|
||||||
uint8_t buffer[read_size];
|
|
||||||
|
|
||||||
int runs;
|
|
||||||
for (runs=0; runs<tries; runs++){
|
|
||||||
out = reproc_read(
|
|
||||||
self->process,
|
self->process,
|
||||||
REPROC_STREAM_OUT,
|
stream,
|
||||||
buffer,
|
buffer,
|
||||||
read_size
|
read_size
|
||||||
);
|
);
|
||||||
|
|
||||||
if (out >= 0)
|
if (out >= 0)
|
||||||
break;
|
luaL_addsize(&b, out);
|
||||||
}
|
luaL_pushresult(&b);
|
||||||
|
|
||||||
if (out == REPROC_EPIPE) {
|
if (out == REPROC_EPIPE) {
|
||||||
reproc_kill(self->process);
|
kill_process(self);
|
||||||
reproc_destroy(self->process);
|
ASSERT_REPROC_ERRNO(L, out);
|
||||||
self->process = NULL;
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
} else if(out > 0) {
|
|
||||||
lua_pushlstring(L, (const char*) buffer, out);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_read(lua_State* L)
|
static int f_read_stdout(lua_State* L)
|
||||||
{
|
{
|
||||||
return g_read(L, REPROC_STREAM_OUT);
|
return g_read(L, REPROC_STREAM_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_read_errors(lua_State* L)
|
static int f_read_stderr(lua_State* L)
|
||||||
{
|
{
|
||||||
return g_read(L, REPROC_STREAM_ERR);
|
return g_read(L, REPROC_STREAM_ERR);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_write(lua_State* L)
|
static int f_read(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
int stream = luaL_checknumber(L, 2);
|
||||||
|
lua_remove(L, 2);
|
||||||
|
if (stream > REPROC_STREAM_ERR)
|
||||||
|
L_RETURN_REPROC_ERROR(L, REPROC_EINVAL);
|
||||||
|
|
||||||
|
return g_read(L, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int f_write(lua_State* L)
|
||||||
|
{
|
||||||
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
|
||||||
size_t data_size = 0;
|
size_t data_size = 0;
|
||||||
const char* data = luaL_checklstring(L, 2, &data_size);
|
const char* data = luaL_checklstring(L, 2, &data_size);
|
||||||
|
|
||||||
int out = 0;
|
int out = reproc_write(
|
||||||
|
|
||||||
out = reproc_write(
|
|
||||||
self->process,
|
self->process,
|
||||||
(uint8_t*) data,
|
(uint8_t*) data,
|
||||||
data_size
|
data_size
|
||||||
);
|
);
|
||||||
|
|
||||||
if (out == REPROC_EPIPE) {
|
if (out == REPROC_EPIPE) {
|
||||||
reproc_kill(self->process);
|
kill_process(self);
|
||||||
reproc_destroy(self->process);
|
L_RETURN_REPROC_ERROR(L, out);
|
||||||
self->process = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lua_pushnumber(L, out);
|
lua_pushnumber(L, out);
|
||||||
} else {
|
|
||||||
lua_pushnumber(L, REPROC_EPIPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_close_stream(lua_State* L)
|
static int f_close_stream(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
|
||||||
size_t stream = luaL_checknumber(L, 2);
|
|
||||||
|
|
||||||
|
int stream = luaL_checknumber(L, 2);
|
||||||
int out = reproc_close(self->process, stream);
|
int out = reproc_close(self->process, stream);
|
||||||
|
ASSERT_REPROC_ERRNO(L, out);
|
||||||
|
|
||||||
lua_pushnumber(L, out);
|
lua_pushboolean(L, 1);
|
||||||
} else {
|
|
||||||
lua_pushnumber(L, REPROC_EINVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_wait(lua_State* L)
|
static int f_wait(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
int timeout = luaL_optnumber(L, 2, 0);
|
||||||
size_t timeout = luaL_checknumber(L, 2);
|
|
||||||
|
|
||||||
int out = reproc_wait(self->process, timeout);
|
int ret = poll_process(self, timeout);
|
||||||
|
// negative returncode is also used for signals on POSIX
|
||||||
if(out >= 0){
|
if (ret == REPROC_ETIMEDOUT)
|
||||||
reproc_destroy(self->process);
|
L_RETURN_REPROC_ERROR(L, ret);
|
||||||
self->process = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
lua_pushnumber(L, out);
|
|
||||||
} else {
|
|
||||||
lua_pushnumber(L, REPROC_EINVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
lua_pushnumber(L, ret);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_terminate(lua_State* L)
|
static int f_terminate(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
|
||||||
int out = reproc_terminate(self->process);
|
int out = reproc_terminate(self->process);
|
||||||
|
ASSERT_REPROC_ERRNO(L, out);
|
||||||
|
|
||||||
if(out < 0){
|
poll_process(self, 0);
|
||||||
lua_pushnumber(L, out);
|
|
||||||
} else {
|
|
||||||
reproc_destroy(self->process);
|
|
||||||
self->process = NULL;
|
|
||||||
lua_pushboolean(L, 1);
|
lua_pushboolean(L, 1);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lua_pushnumber(L, REPROC_EINVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_kill(lua_State* L)
|
static int f_kill(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
|
||||||
int out = reproc_kill(self->process);
|
int out = reproc_kill(self->process);
|
||||||
|
ASSERT_REPROC_ERRNO(L, out);
|
||||||
|
|
||||||
if(out < 0){
|
poll_process(self, 0);
|
||||||
lua_pushnumber(L, out);
|
|
||||||
} else {
|
|
||||||
reproc_destroy(self->process);
|
|
||||||
self->process = NULL;
|
|
||||||
lua_pushboolean(L, 1);
|
lua_pushboolean(L, 1);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lua_pushnumber(L, REPROC_EINVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int process_running(lua_State* L)
|
static int f_running(lua_State* L)
|
||||||
{
|
{
|
||||||
process_t* self = (process_t*) lua_touserdata(L, 1);
|
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||||
|
|
||||||
if(self->process){
|
poll_process(self, 0);
|
||||||
lua_pushboolean(L, 1);
|
lua_pushboolean(L, self->running);
|
||||||
} else {
|
|
||||||
lua_pushboolean(L, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct luaL_Reg process_methods[] = {
|
static const struct luaL_Reg lib[] = {
|
||||||
{ "__gc", process_gc},
|
|
||||||
{"start", process_start},
|
{"start", process_start},
|
||||||
{"pid", process_pid},
|
|
||||||
{"read", process_read},
|
|
||||||
{"read_errors", process_read_errors},
|
|
||||||
{"write", process_write},
|
|
||||||
{"close_stream", process_close_stream},
|
|
||||||
{"wait", process_wait},
|
|
||||||
{"terminate", process_terminate},
|
|
||||||
{"kill", process_kill},
|
|
||||||
{"running", process_running},
|
|
||||||
{NULL, NULL}
|
|
||||||
};
|
|
||||||
|
|
||||||
static const struct luaL_Reg process[] = {
|
|
||||||
{"new", process_new},
|
|
||||||
{"strerror", process_strerror},
|
{"strerror", process_strerror},
|
||||||
{"ERROR_PIPE", NULL},
|
{"__gc", f_gc},
|
||||||
{"ERROR_WOULDBLOCK", NULL},
|
{"__tostring", f_tostring},
|
||||||
{"ERROR_TIMEDOUT", NULL},
|
{"pid", f_pid},
|
||||||
{"ERROR_INVALID", NULL},
|
{"returncode", f_returncode},
|
||||||
{"STREAM_STDIN", NULL},
|
{"read", f_read},
|
||||||
{"STREAM_STDOUT", NULL},
|
{"read_stdout", f_read_stdout},
|
||||||
{"STREAM_STDERR", NULL},
|
{"read_stderr", f_read_stderr},
|
||||||
{"WAIT_INFINITE", NULL},
|
{"write", f_write},
|
||||||
{"WAIT_DEADLINE", NULL},
|
{"close_stream", f_close_stream},
|
||||||
|
{"wait", f_wait},
|
||||||
|
{"terminate", f_terminate},
|
||||||
|
{"kill", f_kill},
|
||||||
|
{"running", f_running},
|
||||||
{NULL, NULL}
|
{NULL, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
int luaopen_process(lua_State *L)
|
int luaopen_process(lua_State *L)
|
||||||
{
|
{
|
||||||
luaL_newmetatable(L, API_TYPE_PROCESS);
|
luaL_newmetatable(L, API_TYPE_PROCESS);
|
||||||
luaL_setfuncs(L, process_methods, 0);
|
luaL_setfuncs(L, lib, 0);
|
||||||
lua_pushvalue(L, -1);
|
lua_pushvalue(L, -1);
|
||||||
lua_setfield(L, -2, "__index");
|
lua_setfield(L, -2, "__index");
|
||||||
|
|
||||||
luaL_newlib(L, process);
|
// constants
|
||||||
|
L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL);
|
||||||
|
L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT);
|
||||||
|
L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE);
|
||||||
|
L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM);
|
||||||
|
L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK);
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_EPIPE);
|
L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE);
|
||||||
lua_setfield(L, -2, "ERROR_PIPE");
|
L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE);
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_EWOULDBLOCK);
|
L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN);
|
||||||
lua_setfield(L, -2, "ERROR_WOULDBLOCK");
|
L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT);
|
||||||
|
L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR);
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_ETIMEDOUT);
|
L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT);
|
||||||
lua_setfield(L, -2, "ERROR_TIMEDOUT");
|
L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE);
|
||||||
|
L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT);
|
||||||
lua_pushnumber(L, REPROC_EINVAL);
|
L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD);
|
||||||
lua_setfield(L, -2, "ERROR_INVALID");
|
L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT);
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_STREAM_IN);
|
|
||||||
lua_setfield(L, -2, "STREAM_STDIN");
|
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_STREAM_OUT);
|
|
||||||
lua_setfield(L, -2, "STREAM_STDOUT");
|
|
||||||
|
|
||||||
lua_pushnumber(L, REPROC_STREAM_ERR);
|
|
||||||
lua_setfield(L, -2, "STREAM_STDERR");
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue