Merged dev to master.

This commit is contained in:
Adam Harrison
2021-07-20 14:39:50 -04:00
parent 7605b626e8
commit 0777a6f0b8
11 changed files with 747 additions and 377 deletions
+12 -3
View File
@@ -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
+61 -3
View File
@@ -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,
+26 -1
View File
@@ -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
+37 -11
View File
@@ -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
+42 -38
View File
@@ -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
+1
View File
@@ -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",
+7 -7
View File
@@ -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
+251 -30
View File
@@ -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",
+11
View File
@@ -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",
},
}