When a command is performed with parameters, those are now passed to the predicate. The predicate can then return, after the validity boolean, any other value that will then be passed to the actual command. If the predicate only returns the validity boolean, the original parameters are passed through to the actual command. This allows predicates to manipulate the received parameters, and allows them to pass the result of an expensive computation to the actual command, which won't have to recalculate it. String and table predicates will now also return `core.active_view`.
371 lines
10 KiB
Lua
371 lines
10 KiB
Lua
-- mod-version:3
|
|
local core = require "core"
|
|
local command = require "core.command"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local core_syntax = require "core.syntax"
|
|
local DocView = require "core.docview"
|
|
local Doc = require "core.doc"
|
|
|
|
local cache = setmetatable({}, { __mode = "k" })
|
|
local comments_cache = {}
|
|
local auto_detect_max_lines = 150
|
|
|
|
|
|
local function indent_occurrences_more_than_once(stat, idx)
|
|
if stat[idx-1] and stat[idx-1] == stat[idx] then
|
|
return true
|
|
elseif stat[idx+1] and stat[idx+1] == stat[idx] then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
local function optimal_indent_from_stat(stat)
|
|
if #stat == 0 then return nil, 0 end
|
|
table.sort(stat, function(a, b) return a > b end)
|
|
local best_indent = 0
|
|
local best_score = 0
|
|
local count = #stat
|
|
for x=1, count do
|
|
local indent = stat[x]
|
|
local score = 0
|
|
for y=1, count do
|
|
if y ~= x and stat[y] % indent == 0 then
|
|
score = score + 1
|
|
elseif
|
|
indent > stat[y]
|
|
and
|
|
indent_occurrences_more_than_once(stat, y)
|
|
then
|
|
score = 0
|
|
break
|
|
end
|
|
end
|
|
if score > best_score then
|
|
best_indent = indent
|
|
best_score = score
|
|
end
|
|
if score > 0 then
|
|
break
|
|
end
|
|
end
|
|
return best_score > 0 and best_indent or nil, best_score
|
|
end
|
|
|
|
|
|
local function escape_comment_tokens(token)
|
|
local special_chars = "*-%[].()+?^$"
|
|
local escaped = ""
|
|
for x=1, token:len() do
|
|
local found = false
|
|
for y=1, special_chars:len() do
|
|
if token:sub(x, x) == special_chars:sub(y, y) then
|
|
escaped = escaped .. "%" .. token:sub(x, x)
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
if not found then
|
|
escaped = escaped .. token:sub(x, x)
|
|
end
|
|
end
|
|
return escaped
|
|
end
|
|
|
|
|
|
local function get_comment_patterns(syntax)
|
|
if comments_cache[syntax] then
|
|
if #comments_cache[syntax] > 0 then
|
|
return comments_cache[syntax]
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
local comments = {}
|
|
for idx=1, #syntax.patterns do
|
|
local pattern = syntax.patterns[idx]
|
|
local startp = ""
|
|
if
|
|
type(pattern.type) == "string"
|
|
and
|
|
(pattern.type == "comment" or pattern.type == "string")
|
|
then
|
|
local not_is_string = pattern.type ~= "string"
|
|
if pattern.pattern then
|
|
startp = type(pattern.pattern) == "table"
|
|
and pattern.pattern[1] or pattern.pattern
|
|
if not_is_string and startp:sub(1, 1) ~= "^" then
|
|
startp = "^%s*" .. startp
|
|
elseif not_is_string then
|
|
startp = "^%s*" .. startp:sub(2, startp:len())
|
|
end
|
|
if type(pattern.pattern) == "table" then
|
|
table.insert(comments, {"p", startp, pattern.pattern[2]})
|
|
elseif not_is_string then
|
|
table.insert(comments, {"p", startp})
|
|
end
|
|
elseif pattern.regex then
|
|
startp = type(pattern.regex) == "table"
|
|
and pattern.regex[1] or pattern.regex
|
|
if not_is_string and startp:sub(1, 1) ~= "^" then
|
|
startp = "^\\s*" .. startp
|
|
elseif not_is_string then
|
|
startp = "^\\s*" .. startp:sub(2, startp:len())
|
|
end
|
|
if type(pattern.regex) == "table" then
|
|
table.insert(comments, {
|
|
"r", regex.compile(startp), regex.compile(pattern.regex[2])
|
|
})
|
|
elseif not_is_string then
|
|
table.insert(comments, {"r", regex.compile(startp)})
|
|
end
|
|
end
|
|
elseif pattern.syntax then
|
|
local subsyntax = type(pattern.syntax) == 'table' and pattern.syntax
|
|
or core_syntax.get("file"..pattern.syntax, "")
|
|
local sub_comments = get_comment_patterns(subsyntax)
|
|
if sub_comments then
|
|
for s=1, #sub_comments do
|
|
table.insert(comments, sub_comments[s])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if #comments == 0 then
|
|
local single_line_comment = syntax.comment
|
|
and escape_comment_tokens(syntax.comment) or nil
|
|
local block_comment = nil
|
|
if syntax.block_comment then
|
|
block_comment = {
|
|
escape_comment_tokens(syntax.block_comment[1]),
|
|
escape_comment_tokens(syntax.block_comment[2])
|
|
}
|
|
end
|
|
if single_line_comment then
|
|
table.insert(comments, {"p", "^%s*" .. single_line_comment})
|
|
end
|
|
if block_comment then
|
|
table.insert(comments, {"p", "^%s*" .. block_comment[1], block_comment[2]})
|
|
end
|
|
end
|
|
comments_cache[syntax] = comments
|
|
if #comments > 0 then
|
|
return comments
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
local function get_non_empty_lines(syntax, lines)
|
|
return coroutine.wrap(function()
|
|
local comments = get_comment_patterns(syntax)
|
|
|
|
local i = 0
|
|
local end_regex = nil
|
|
local end_pattern = nil
|
|
local inside_comment = false
|
|
for _, line in ipairs(lines) do
|
|
if line:gsub("^%s+", "") ~= "" then
|
|
local is_comment = false
|
|
if comments then
|
|
if not inside_comment then
|
|
for c=1, #comments do
|
|
local comment = comments[c]
|
|
if comment[1] == "p" then
|
|
if comment[3] then
|
|
local start, ending = line:find(comment[2])
|
|
if start then
|
|
if not line:find(comment[3], ending+1) then
|
|
is_comment = true
|
|
inside_comment = true
|
|
end_pattern = comment[3]
|
|
end
|
|
break
|
|
end
|
|
elseif line:find(comment[2]) then
|
|
is_comment = true
|
|
break
|
|
end
|
|
else
|
|
if comment[3] then
|
|
local start, ending = regex.match(
|
|
comment[2], line, 1, regex.ANCHORED
|
|
)
|
|
if start then
|
|
if not regex.match(
|
|
comment[3], line, ending+1, regex.ANCHORED
|
|
)
|
|
then
|
|
is_comment = true
|
|
inside_comment = true
|
|
end_regex = comment[3]
|
|
end
|
|
break
|
|
end
|
|
elseif regex.match(comment[2], line, 1, regex.ANCHORED) then
|
|
is_comment = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
elseif end_pattern and line:find(end_pattern) then
|
|
is_comment = true
|
|
inside_comment = false
|
|
end_pattern = nil
|
|
elseif end_regex and regex.match(end_regex, line) then
|
|
is_comment = true
|
|
inside_comment = false
|
|
end_regex = nil
|
|
end
|
|
end
|
|
if
|
|
not is_comment
|
|
and
|
|
not inside_comment
|
|
then
|
|
i = i + 1
|
|
coroutine.yield(i, line)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
|
|
local function detect_indent_stat(doc)
|
|
local stat = {}
|
|
local tab_count = 0
|
|
local runs = 1
|
|
local max_lines = auto_detect_max_lines
|
|
for i, text in get_non_empty_lines(doc.syntax, doc.lines) do
|
|
local spaces = text:match("^ +")
|
|
if spaces then table.insert(stat, spaces:len()) end
|
|
local tabs = text:match("^\t+")
|
|
if tabs then tab_count = tab_count + 1 end
|
|
-- if nothing found for first lines try at least 4 more times
|
|
if i == max_lines and runs < 5 and #stat == 0 and tab_count == 0 then
|
|
max_lines = max_lines + auto_detect_max_lines
|
|
runs = runs + 1
|
|
-- Stop parsing when files is very long. Not needed for euristic determination.
|
|
elseif i > max_lines then break end
|
|
end
|
|
local indent, score = optimal_indent_from_stat(stat)
|
|
if tab_count > score then
|
|
return "hard", config.indent_size, tab_count
|
|
else
|
|
return "soft", indent or config.indent_size, score or 0
|
|
end
|
|
end
|
|
|
|
|
|
local function update_cache(doc)
|
|
local type, size, score = detect_indent_stat(doc)
|
|
local score_threshold = 2
|
|
if score < score_threshold then
|
|
-- use default values
|
|
type = config.tab_type
|
|
size = config.indent_size
|
|
end
|
|
cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) }
|
|
doc.indent_info = cache[doc]
|
|
end
|
|
|
|
|
|
local new = Doc.new
|
|
function Doc:new(...)
|
|
new(self, ...)
|
|
update_cache(self)
|
|
end
|
|
|
|
local clean = Doc.clean
|
|
function Doc:clean(...)
|
|
clean(self, ...)
|
|
local _, _, confirmed = self:get_indent_info()
|
|
if not confirmed then
|
|
update_cache(self)
|
|
end
|
|
end
|
|
|
|
|
|
local function set_indent_type(doc, type)
|
|
local _, indent_size = doc:get_indent_info()
|
|
cache[doc] = {
|
|
type = type,
|
|
size = indent_size,
|
|
confirmed = true
|
|
}
|
|
doc.indent_info = cache[doc]
|
|
end
|
|
|
|
local function set_indent_type_command(dv)
|
|
core.command_view:enter("Specify indent style for this file", {
|
|
submit = function(value)
|
|
local doc = dv.doc
|
|
value = value:lower()
|
|
set_indent_type(doc, value == "tabs" and "hard" or "soft")
|
|
end,
|
|
suggest = function(text)
|
|
return common.fuzzy_match({"tabs", "spaces"}, text)
|
|
end,
|
|
validate = function(text)
|
|
local t = text:lower()
|
|
return t == "tabs" or t == "spaces"
|
|
end
|
|
})
|
|
end
|
|
|
|
|
|
local function set_indent_size(doc, size)
|
|
local indent_type = doc:get_indent_info()
|
|
cache[doc] = {
|
|
type = indent_type,
|
|
size = size,
|
|
confirmed = true
|
|
}
|
|
doc.indent_info = cache[doc]
|
|
end
|
|
|
|
local function set_indent_size_command(dv)
|
|
core.command_view:enter("Specify indent size for current file", {
|
|
submit = function(value)
|
|
value = math.floor(tonumber(value))
|
|
local doc = dv.doc
|
|
set_indent_size(doc, value)
|
|
end,
|
|
validate = function(value)
|
|
value = tonumber(value)
|
|
return value ~= nil and value >= 1
|
|
end
|
|
})
|
|
end
|
|
|
|
|
|
command.add("core.docview", {
|
|
["indent:set-file-indent-type"] = set_indent_type_command,
|
|
["indent:set-file-indent-size"] = set_indent_size_command
|
|
})
|
|
|
|
command.add(
|
|
function()
|
|
return core.active_view:is(DocView)
|
|
and cache[core.active_view.doc]
|
|
and cache[core.active_view.doc].type == "soft"
|
|
end, {
|
|
["indent:switch-file-to-tabs-indentation"] = function()
|
|
set_indent_type(core.active_view.doc, "hard")
|
|
end
|
|
})
|
|
|
|
command.add(
|
|
function()
|
|
return core.active_view:is(DocView)
|
|
and cache[core.active_view.doc]
|
|
and cache[core.active_view.doc].type == "hard"
|
|
end, {
|
|
["indent:switch-file-to-spaces-indentation"] = function()
|
|
set_indent_type(core.active_view.doc, "soft")
|
|
end
|
|
})
|