From 0777a6f0b8fd5d4392973d403b7ca6b54eee1ac4 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 20 Jul 2021 14:39:50 -0400 Subject: [PATCH] Merged dev to master. --- data/core/commands/core.lua | 15 +- data/core/commands/doc.lua | 64 +++- data/core/common.lua | 27 +- data/core/doc/init.lua | 48 ++- data/core/init.lua | 80 ++--- data/core/keymap.lua | 1 + data/core/regex.lua | 14 +- data/plugins/autocomplete.lua | 281 +++++++++++++++-- data/plugins/language_c.lua | 11 + src/api/process.c | 575 +++++++++++++++++----------------- src/api/regex.c | 8 +- 11 files changed, 747 insertions(+), 377 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 859fb066..ac30fe20 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -104,11 +104,20 @@ command.add(nil, { end, function (text) return common.home_encode_list(common.path_suggest(common.home_expand(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 - 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 - core.error("Cannot open %q, is a folder", text) + core.error("Cannot open %s, is a folder", text) else return true end diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index ca39bfde..5ddcd507 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -43,11 +43,67 @@ local function append_line_if_last_line(line) end 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 core.log("Saved \"%s\"", saved_filename) end +-- returns the size of the original indent, and the indent +-- in your config format, rounded either up or down +local function get_line_indent(line, rnd_up) + local _, e = line:find("^[ \t]+") + local soft_tab = string.rep(" ", config.indent_size) + if config.tab_type == "hard" then + local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" + return e, indent:gsub(" +", rnd_up and "\t" or "") + else + local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" + local number = #indent / #soft_tab + return e, indent:sub(1, + (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) + end +end + +-- un/indents text; behaviour varies based on selection and un/indent. +-- * if there's a selection, it will stay static around the +-- text for both indenting and unindenting. +-- * if you are in the beginning whitespace of a line, and are indenting, the +-- cursor will insert the exactly appropriate amount of spaces, and jump the +-- cursor to the beginning of first non whitespace characters +-- * if you are not in the beginning whitespace of a line, and you indent, it +-- inserts the appropriate whitespace, as if you typed them normally. +-- * if you are unindenting, the cursor will jump to the start of the line, +-- and remove the appropriate amount of spaces (or a tab). +local function indent_text(unindent) + local text = get_indent_string() + local line1, col1, line2, col2, swap = doc_multiline_selection(true) + local _, se = doc().lines[line1]:find("^[ \t]+") + local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) + if unindent or doc():has_selection() or in_beginning_whitespace then + local l1d, l2d = #doc().lines[line1], #doc().lines[line2] + for line = line1, line2 do + local e, rnded = get_line_indent(doc().lines[line], unindent) + doc():remove(line, 1, line, (e or 0) + 1) + doc():insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end + l1d, l2d = #doc().lines[line1] - l1d, #doc().lines[line2] - l2d + if (unindent or in_beginning_whitespace) and not doc():has_selection() then + local start_cursor = (se and se + 1 or 1) + l1d or #(doc().lines[line1]) + doc():set_selection(line1, start_cursor, line2, start_cursor, swap) + else + doc():set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) + end + else + doc():text_input(text) + end +end + local function cut_or_copy(delete) local full_text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do @@ -363,12 +419,14 @@ local commands = { end core.command_view:set_text(old_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) if filename ~= old_filename then os.remove(old_filename) end - end, common.path_suggest) + end, function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end) end, diff --git a/data/core/common.lua b/data/core/common.lua index 6b9dff7d..ce450570 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -230,6 +230,12 @@ function common.basename(path) 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) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 @@ -279,8 +285,27 @@ local function split_on_slash(s, sep_pattern) 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) - return filename and string.find(filename, path .. PATHSEP, 1, true) == 1 + return string.find(filename, path .. PATHSEP, 1, true) == 1 end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index c33ade5a..652b5545 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -17,10 +17,34 @@ local function split_lines(text) return res end -function Doc:new(filename) + +local function splice(t, at, remove, insert) + insert = insert or {} + local offset = #insert - remove + local old_len = #t + if offset < 0 then + for i = at - offset, old_len - offset do + t[i + offset] = t[i] + end + elseif offset > 0 then + for i = old_len, at, -1 do + t[i + offset] = t[i] + end + end + for i, item in ipairs(insert) do + t[at + i - 1] = item + end +end + + +function Doc:new(filename, abs_filename, new_file) + self.new_file = new_file self:reset() if filename then - self:load(filename) + self:set_filename(filename, abs_filename) + if not new_file then + self:load(filename) + end end end @@ -47,16 +71,15 @@ function Doc:reset_syntax() end -function Doc:set_filename(filename) +function Doc:set_filename(filename, abs_filename) self.filename = filename - self.abs_filename = system.absolute_path(filename) + self.abs_filename = abs_filename end function Doc:load(filename) local fp = assert( io.open(filename, "rb") ) self:reset() - self:set_filename(filename) self.lines = {} for line in fp:lines() do if line:byte(-1) == 13 then @@ -73,17 +96,20 @@ function Doc:load(filename) end -function Doc:save(filename) - filename = filename or assert(self.filename, "no filename set to default to") +function Doc:save(filename, abs_filename) + 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") ) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) end fp:close() - if filename then - self:set_filename(filename) - end + self:set_filename(filename, abs_filename) + self.new_file = false self:reset_syntax() self:clean() end @@ -95,7 +121,7 @@ end 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 diff --git a/data/core/init.lua b/data/core/init.lua index c13151ad..9bdbb36c 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -620,24 +620,6 @@ do 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) if force then delete_temp_files() @@ -695,24 +677,27 @@ function core.load_plugins() userdir = {dir = USERDIR, 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 files = system.list_dir(plugin_dir) - for _, filename in ipairs(files or {}) do - local basename = filename:match("(.-)%.lua$") or filename - local version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if not version_match then - 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 - ls[#ls + 1] = filename - end - if version_match and config.plugins[basename] ~= false then - local modname = "plugins." .. basename - local ok = core.try(require, modname) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end - if not ok then - no_errors = false - end + for _, filename in ipairs(system.list_dir(plugin_dir) 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 version_match = check_plugin_version(plugin_dir .. '/' .. filename) + if not version_match then + core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) + local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins + table.insert(list, filename) + end + if version_match and core.plugins[basename] ~= false then + local ok = core.try(require, "plugins." .. basename) + if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + if not ok then + no_errors = false end end end @@ -809,10 +794,30 @@ function core.normalize_to_project_dir(filename) 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) + local new_file = not filename or not system.get_file_info(filename) + local abs_filename if filename then + -- normalize filename and set absolute filename then -- 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 if doc.abs_filename and abs_filename == doc.abs_filename then return doc @@ -820,8 +825,7 @@ function core.open_doc(filename) end end -- no existing doc for filename; create new - filename = filename and core.normalize_to_project_dir(filename) - local doc = Doc(filename) + local doc = Doc(filename, abs_filename, new_file) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) return doc diff --git a/data/core/keymap.lua b/data/core/keymap.lua index c402f37b..86be82b1 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -108,6 +108,7 @@ keymap.add_direct { ["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", + ["f11"] = "core:toggle-fullscreen", ["alt+shift+j"] = "root:split-left", ["alt+shift+l"] = "root:split-right", diff --git a/data/core/regex.lua b/data/core/regex.lua index 19c59164..19306e04 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -1,5 +1,5 @@ --- So that in addition to regex.gsub(pattern, string), we can also do +-- So that in addition to regex.gsub(pattern, string), we can also do -- pattern:gsub(string). regex.__index = function(table, key) return regex[key]; end @@ -9,7 +9,7 @@ regex.match = function(pattern_string, string, offset, options) return regex.cmatch(pattern, string, offset or 1, options or 0) end --- Will iterate back through any UTF-8 bytes so that we don't replace bits +-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- mid character. local function previous_character(str, index) local byte @@ -32,7 +32,7 @@ end -- Build off matching. For now, only support basic replacements, but capture -- groupings should be doable. We can even have custom group replacements and --- transformations and stuff in lua. Currently, this takes group replacements +-- transformations and stuff in lua. Currently, this takes group replacements -- as \1 - \9. -- Should work on UTF-8 text. regex.gsub = function(pattern_string, str, replacement) @@ -48,8 +48,8 @@ regex.gsub = function(pattern_string, str, replacement) if #indices > 2 then for i = 1, (#indices/2 - 1) do currentReplacement = string.gsub( - currentReplacement, - "\\" .. i, + currentReplacement, + "\\" .. i, str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) ) end @@ -57,10 +57,10 @@ regex.gsub = function(pattern_string, str, replacement) currentReplacement = string.gsub(currentReplacement, "\\%d", "") table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) if indices[1] > 1 then - result = result .. + result = result .. str:sub(1, previous_character(str, indices[1])) .. currentReplacement else - result = result .. currentReplacement + result = result .. currentReplacement end str = str:sub(indices[2]) end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 867d5360..4703a63a 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -8,25 +8,65 @@ local keymap = require "core.keymap" local translate = require "core.doc.translate" local RootView = require "core.rootview" 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 = {} -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 } -function autocomplete.add(t) +function autocomplete.add(t, triggered_manually) local items = {} for text, info in pairs(t.items) do - info = (type(info) == "string") and info - table.insert(items, setmetatable({ text = text, info = info }, mt)) + 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 + table.insert(items, setmetatable({ text = text, info = info }, mt)) + end + end + + 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 - autocomplete.map[t.name] = { files = t.files or ".*", items = items } end -local max_symbols = config.max_symbols or 2000 +-- +-- Thread that scans open document symbols and cache them +-- +local max_symbols = config.max_symbols core.add_thread(function() local cache = setmetatable({}, { __mode = "k" }) @@ -109,16 +149,39 @@ local last_line, last_col local function reset_suggestions() suggestions_idx = 1 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 +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 doc = core.active_view.doc 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 local items = {} - for _, v in pairs(autocomplete.map) do + for _, v in pairs(map) do if common.match_pattern(filename, v.files) then for _, item in pairs(v.items) do table.insert(items, item) @@ -138,7 +201,6 @@ local function update_suggestions() end end - local function get_partial_symbol() local doc = core.active_view.doc local line2, col2 = doc:get_selection() @@ -146,14 +208,12 @@ local function get_partial_symbol() return doc:get_text(line1, col1, line2, col2) end - local function get_active_view() if getmetatable(core.active_view) == DocView then return core.active_view end end - local function get_suggestions_rect(av) if #suggestions == 0 then return 0, 0, 0, 0 @@ -175,15 +235,67 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) 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 x - style.padding.x, y - style.padding.y, max_width + style.padding.x * 2, - #suggestions * (th + style.padding.y) + style.padding.y + max_items * (th + style.padding.y) + style.padding.y 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) + if #suggestions <= 0 then + return + end + + local ah = config.plugins.autocomplete.max_height + -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) 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 lh = font:get_height() + style.padding.y 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 common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) 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) end 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 + + 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 on_text_input = RootView.on_text_input -local update = RootView.update -local draw = RootView.draw - - -RootView.on_text_input = function(...) - on_text_input(...) - +local function show_autocomplete() local av = get_active_view() if av then -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= 3 then + + if #partial >= config.plugins.autocomplete.min_len or triggered_manually then update_suggestions() - last_line, last_col = av.doc:get_selection() + + if not triggered_manually then + 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 reset_suggestions() end @@ -233,6 +381,30 @@ RootView.on_text_input = function(...) 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(...) update(...) @@ -241,13 +413,19 @@ RootView.update = function(...) if av then -- reset suggestions if caret was moved local line, col = av.doc:get_selection() - if line ~= last_line or col ~= last_col then - reset_suggestions() + + if not triggered_manually then + if line ~= last_line or col ~= last_col then + reset_suggestions() + end + else + if line ~= last_line or col < last_col then + reset_suggestions() + end end end end - RootView.draw = function(...) draw(...) @@ -258,12 +436,53 @@ RootView.draw = function(...) 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() return get_active_view() and #suggestions > 0 end - command.add(predicate, { ["autocomplete:complete"] = function() local doc = core.active_view.doc @@ -288,7 +507,9 @@ command.add(predicate, { end, }) - +-- +-- Keymaps +-- keymap.add { ["tab"] = "autocomplete:complete", ["up"] = "autocomplete:previous", diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 836e1692..f55140c9 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -55,6 +55,17 @@ syntax.add { ["true"] = "literal", ["false"] = "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", }, } diff --git a/src/api/process.c b/src/api/process.c index 84d86b6e..4b018e4c 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -10,28 +10,166 @@ #include #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 { reproc_t * process; - lua_State* L; - + bool running; + int returncode; } 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( - L, sizeof(process_t) + int ret = reproc_wait(proc->process, timeout); + 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; - self->L = L; + return ret; +} - luaL_getmetatable(L, API_TYPE_PROCESS); - lua_setmetatable(L, -2); +static int process_start(lua_State* L) +{ + 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; } @@ -39,24 +177,20 @@ static int process_strerror(lua_State* L) { int error_code = luaL_checknumber(L, 1); - if(error_code){ - lua_pushstring( - L, - reproc_strerror(error_code) - ); - } else { + if (error_code < 0) + lua_pushstring(L, reproc_strerror(error_code)); + else lua_pushnil(L); - } - + 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); - if(self->process){ - reproc_kill(self->process); + if(self->process) { + kill_process(self); reproc_destroy(self->process); self->process = NULL; } @@ -64,330 +198,211 @@ static int process_gc(lua_State* L) 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_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); - } + luaL_checkudata(L, 1, API_TYPE_PROCESS); + lua_pushliteral(L, API_TYPE_PROCESS); 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){ - int id = reproc_pid(self->process); - - if(id > 0){ - lua_pushnumber(L, id); - } else { - lua_pushnumber(L, 0); - } - } else { - lua_pushnumber(L, 0); - } + lua_pushnumber(L, reproc_pid(self->process)); + return 1; +} + +static int f_returncode(lua_State *L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + int ret = poll_process(self, 0); + + if (self->running) + lua_pushnil(L); + else + lua_pushnumber(L, ret); return 1; } 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){ - int read_size = READ_BUF_SIZE; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } + luaL_Buffer b; + uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size); - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } + int out = reproc_read( + self->process, + stream, + buffer, + read_size + ); - int out = 0; - uint8_t buffer[read_size]; + if (out >= 0) + luaL_addsize(&b, out); + luaL_pushresult(&b); - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_OUT, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - 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); + if (out == REPROC_EPIPE) { + kill_process(self); + ASSERT_REPROC_ERRNO(L, out); } return 1; } -static int process_read(lua_State* L) +static int f_read_stdout(lua_State* L) { 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); } -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); - if(self->process){ - size_t data_size = 0; - const char* data = luaL_checklstring(L, 2, &data_size); + return g_read(L, stream); +} - int out = 0; +static int f_write(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - out = reproc_write( - self->process, - (uint8_t*) data, - data_size - ); + size_t data_size = 0; + const char* data = luaL_checklstring(L, 2, &data_size); - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EPIPE); + int out = reproc_write( + self->process, + (uint8_t*) data, + data_size + ); + if (out == REPROC_EPIPE) { + kill_process(self); + L_RETURN_REPROC_ERROR(L, out); } + lua_pushnumber(L, out); + return 1; +} + +static int f_close_stream(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int stream = luaL_checknumber(L, 2); + int out = reproc_close(self->process, stream); + ASSERT_REPROC_ERRNO(L, out); + + lua_pushboolean(L, 1); + return 1; +} + +static int f_wait(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int timeout = luaL_optnumber(L, 2, 0); + + int ret = poll_process(self, timeout); + // negative returncode is also used for signals on POSIX + if (ret == REPROC_ETIMEDOUT) + L_RETURN_REPROC_ERROR(L, ret); + + lua_pushnumber(L, ret); + return 1; +} + +static int f_terminate(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int out = reproc_terminate(self->process); + ASSERT_REPROC_ERRNO(L, out); + + poll_process(self, 0); + lua_pushboolean(L, 1); + return 1; } -static int process_close_stream(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){ - size_t stream = luaL_checknumber(L, 2); + int out = reproc_kill(self->process); + ASSERT_REPROC_ERRNO(L, out); - int out = reproc_close(self->process, stream); - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + poll_process(self, 0); + lua_pushboolean(L, 1); return 1; } -static int process_wait(lua_State* L) +static int f_running(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - size_t timeout = luaL_checknumber(L, 2); - - int out = reproc_wait(self->process, timeout); - - if(out >= 0){ - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + poll_process(self, 0); + lua_pushboolean(L, self->running); return 1; } -static int process_terminate(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_terminate(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_kill(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_kill(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_running(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - lua_pushboolean(L, 1); - } else { - lua_pushboolean(L, 0); - } - - return 1; -} - -static const struct luaL_Reg process_methods[] = { - { "__gc", process_gc}, +static const struct luaL_Reg lib[] = { {"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}, - {"ERROR_PIPE", NULL}, - {"ERROR_WOULDBLOCK", NULL}, - {"ERROR_TIMEDOUT", NULL}, - {"ERROR_INVALID", NULL}, - {"STREAM_STDIN", NULL}, - {"STREAM_STDOUT", NULL}, - {"STREAM_STDERR", NULL}, - {"WAIT_INFINITE", NULL}, - {"WAIT_DEADLINE", NULL}, + {"__gc", f_gc}, + {"__tostring", f_tostring}, + {"pid", f_pid}, + {"returncode", f_returncode}, + {"read", f_read}, + {"read_stdout", f_read_stdout}, + {"read_stderr", f_read_stderr}, + {"write", f_write}, + {"close_stream", f_close_stream}, + {"wait", f_wait}, + {"terminate", f_terminate}, + {"kill", f_kill}, + {"running", f_running}, {NULL, NULL} }; int luaopen_process(lua_State *L) { luaL_newmetatable(L, API_TYPE_PROCESS); - luaL_setfuncs(L, process_methods, 0); + luaL_setfuncs(L, lib, 0); lua_pushvalue(L, -1); 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); - lua_setfield(L, -2, "ERROR_PIPE"); + L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE); + L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE); - lua_pushnumber(L, REPROC_EWOULDBLOCK); - lua_setfield(L, -2, "ERROR_WOULDBLOCK"); + L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN); + L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT); + L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR); - lua_pushnumber(L, REPROC_ETIMEDOUT); - lua_setfield(L, -2, "ERROR_TIMEDOUT"); - - lua_pushnumber(L, REPROC_EINVAL); - lua_setfield(L, -2, "ERROR_INVALID"); - - 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"); + L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT); + L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE); + L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT); + L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD); + L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT); return 1; } diff --git a/src/api/regex.c b/src/api/regex.c index a5d17604..1043b1c5 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -74,11 +74,11 @@ static int f_pcre_match(lua_State *L) { } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); if (ovector[0] > ovector[1]) { - /* We must guard against patterns such as /(?=.\K)/ that use \K in an + /* We must guard against patterns such as /(?=.\K)/ that use \K in an assertion to set the start of a match later than its end. In the editor, we just detect this case and give up. */ luaL_error(L, "regex matching error: \\K was used in an assertion to " - " set the match start after its end"); + " set the match start after its end"); pcre2_match_data_free(md); return 0; } @@ -103,8 +103,8 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, LUA_REGISTRYINDEX, "regex"); lua_pushnumber(L, PCRE2_ANCHORED); lua_setfield(L, -2, "ANCHORED"); - lua_pushnumber(L, PCRE2_ANCHORED) ; - lua_setfield(L, -2, "ENDANCHORED"); + lua_pushnumber(L, PCRE2_ANCHORED) ; + lua_setfield(L, -2, "ENDANCHORED"); lua_pushnumber(L, PCRE2_NOTBOL); lua_setfield(L, -2, "NOTBOL"); lua_pushnumber(L, PCRE2_NOTEOL);