Autocomplete plugin improvements (#1519)
* Add icons support to autocomplete plugin * Removed redundant flag check * Added support for non syntax colors * Assert if color name not in style.syntax * Autocomplete plugin improvements * Support suggestion symbols scoping - global: all open documents - local: current document - related: all open documents with same syntax - none: language syntax symbols only * Register style.syntax[] entries as icons * Other related fixes
This commit is contained in:
parent
3f28557aeb
commit
215b8daef7
|
@ -10,6 +10,10 @@ local RootView = require "core.rootview"
|
||||||
local DocView = require "core.docview"
|
local DocView = require "core.docview"
|
||||||
local Doc = require "core.doc"
|
local Doc = require "core.doc"
|
||||||
|
|
||||||
|
---Symbols cache of all open documents
|
||||||
|
---@type table<core.doc, table>
|
||||||
|
local cache = setmetatable({}, { __mode = "k" })
|
||||||
|
|
||||||
config.plugins.autocomplete = common.merge({
|
config.plugins.autocomplete = common.merge({
|
||||||
-- Amount of characters that need to be written for autocomplete
|
-- Amount of characters that need to be written for autocomplete
|
||||||
min_len = 3,
|
min_len = 3,
|
||||||
|
@ -19,8 +23,16 @@ config.plugins.autocomplete = common.merge({
|
||||||
max_suggestions = 100,
|
max_suggestions = 100,
|
||||||
-- Maximum amount of symbols to cache per document
|
-- Maximum amount of symbols to cache per document
|
||||||
max_symbols = 4000,
|
max_symbols = 4000,
|
||||||
|
-- Which symbols to show on the suggestions list: global, local, related, none
|
||||||
|
suggestions_scope = "global",
|
||||||
-- Font size of the description box
|
-- Font size of the description box
|
||||||
desc_font_size = 12,
|
desc_font_size = 12,
|
||||||
|
-- Do not show the icons associated to the suggestions
|
||||||
|
hide_icons = false,
|
||||||
|
-- Position where icons will be displayed on the suggestions list
|
||||||
|
icon_position = "left",
|
||||||
|
-- Do not show the additional information related to a suggestion
|
||||||
|
hide_info = false,
|
||||||
-- The config specification used by gui generators
|
-- The config specification used by gui generators
|
||||||
config_spec = {
|
config_spec = {
|
||||||
name = "Autocomplete",
|
name = "Autocomplete",
|
||||||
|
@ -60,6 +72,26 @@ config.plugins.autocomplete = common.merge({
|
||||||
min = 1000,
|
min = 1000,
|
||||||
max = 10000
|
max = 10000
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label = "Suggestions Scope",
|
||||||
|
description = "Which symbols to show on the suggestions list.",
|
||||||
|
path = "suggestions_scope",
|
||||||
|
type = "selection",
|
||||||
|
default = "global",
|
||||||
|
values = {
|
||||||
|
{"All Documents", "global"},
|
||||||
|
{"Current Document", "local"},
|
||||||
|
{"Related Documents", "related"},
|
||||||
|
{"Known Symbols", "none"}
|
||||||
|
},
|
||||||
|
on_apply = function(value)
|
||||||
|
if value == "global" then
|
||||||
|
for _, doc in ipairs(core.docs) do
|
||||||
|
if cache[doc] then cache[doc] = nil end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label = "Description Font Size",
|
label = "Description Font Size",
|
||||||
description = "Font size of the description box.",
|
description = "Font size of the description box.",
|
||||||
|
@ -67,6 +99,31 @@ config.plugins.autocomplete = common.merge({
|
||||||
type = "number",
|
type = "number",
|
||||||
default = 12,
|
default = 12,
|
||||||
min = 8
|
min = 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Hide Icons",
|
||||||
|
description = "Do not show icons on the suggestions list.",
|
||||||
|
path = "hide_icons",
|
||||||
|
type = "toggle",
|
||||||
|
default = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Icons Position",
|
||||||
|
description = "Position to display icons on the suggestions list.",
|
||||||
|
path = "icon_position",
|
||||||
|
type = "selection",
|
||||||
|
default = "left",
|
||||||
|
values = {
|
||||||
|
{"Left", "left"},
|
||||||
|
{"Right", "Right"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Hide Items Info",
|
||||||
|
description = "Do not show the additional info related to each suggestion.",
|
||||||
|
path = "hide_info",
|
||||||
|
type = "toggle",
|
||||||
|
default = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, config.plugins.autocomplete)
|
}, config.plugins.autocomplete)
|
||||||
|
@ -76,6 +133,7 @@ local autocomplete = {}
|
||||||
autocomplete.map = {}
|
autocomplete.map = {}
|
||||||
autocomplete.map_manually = {}
|
autocomplete.map_manually = {}
|
||||||
autocomplete.on_close = nil
|
autocomplete.on_close = nil
|
||||||
|
autocomplete.icons = {}
|
||||||
|
|
||||||
-- Flag that indicates if the autocomplete box was manually triggered
|
-- Flag that indicates if the autocomplete box was manually triggered
|
||||||
-- with the autocomplete.complete() function to prevent the suggestions
|
-- with the autocomplete.complete() function to prevent the suggestions
|
||||||
|
@ -95,6 +153,7 @@ function autocomplete.add(t, manually_triggered)
|
||||||
{
|
{
|
||||||
text = text,
|
text = text,
|
||||||
info = info.info,
|
info = info.info,
|
||||||
|
icon = info.icon, -- Name of icon to show
|
||||||
desc = info.desc, -- Description shown on item selected
|
desc = info.desc, -- Description shown on item selected
|
||||||
onhover = info.onhover, -- A callback called once when item is hovered
|
onhover = info.onhover, -- A callback called once when item is hovered
|
||||||
onselect = info.onselect, -- A callback called when item is selected
|
onselect = info.onselect, -- A callback called when item is selected
|
||||||
|
@ -119,28 +178,35 @@ end
|
||||||
--
|
--
|
||||||
-- Thread that scans open document symbols and cache them
|
-- Thread that scans open document symbols and cache them
|
||||||
--
|
--
|
||||||
local max_symbols = config.plugins.autocomplete.max_symbols
|
local global_symbols = {}
|
||||||
|
|
||||||
core.add_thread(function()
|
core.add_thread(function()
|
||||||
local cache = setmetatable({}, { __mode = "k" })
|
local function load_syntax_symbols(doc)
|
||||||
|
if doc.syntax and not autocomplete.map["language_"..doc.syntax.name] then
|
||||||
local function get_syntax_symbols(symbols, doc)
|
local symbols = {
|
||||||
if doc.syntax then
|
name = "language_"..doc.syntax.name,
|
||||||
for sym in pairs(doc.syntax.symbols) do
|
files = doc.syntax.files,
|
||||||
symbols[sym] = true
|
items = {}
|
||||||
|
}
|
||||||
|
for name, type in pairs(doc.syntax.symbols) do
|
||||||
|
symbols.items[name] = type
|
||||||
end
|
end
|
||||||
|
autocomplete.add(symbols)
|
||||||
|
return symbols.items
|
||||||
end
|
end
|
||||||
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_symbols(doc)
|
local function get_symbols(doc)
|
||||||
local s = {}
|
local s = {}
|
||||||
get_syntax_symbols(s, doc)
|
local syntax_symbols = load_syntax_symbols(doc)
|
||||||
|
local max_symbols = config.plugins.autocomplete.max_symbols
|
||||||
if doc.disable_symbols then return s end
|
if doc.disable_symbols then return s end
|
||||||
local i = 1
|
local i = 1
|
||||||
local symbols_count = 0
|
local symbols_count = 0
|
||||||
while i <= #doc.lines do
|
while i <= #doc.lines do
|
||||||
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
|
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
|
||||||
if not s[sym] then
|
if not s[sym] and not syntax_symbols[sym] then
|
||||||
symbols_count = symbols_count + 1
|
symbols_count = symbols_count + 1
|
||||||
if symbols_count > max_symbols then
|
if symbols_count > max_symbols then
|
||||||
s = nil
|
s = nil
|
||||||
|
@ -186,14 +252,18 @@ core.add_thread(function()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
-- update symbol set with doc's symbol set
|
-- update symbol set with doc's symbol set
|
||||||
|
if config.plugins.autocomplete.suggestions_scope == "global" then
|
||||||
for sym in pairs(cache[doc].symbols) do
|
for sym in pairs(cache[doc].symbols) do
|
||||||
symbols[sym] = true
|
symbols[sym] = true
|
||||||
end
|
end
|
||||||
|
end
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- update symbols list
|
-- update global symbols list
|
||||||
autocomplete.add { name = "open-docs", items = symbols }
|
if config.plugins.autocomplete.suggestions_scope == "global" then
|
||||||
|
global_symbols = symbols
|
||||||
|
end
|
||||||
|
|
||||||
-- wait for next scan
|
-- wait for next scan
|
||||||
local valid = true
|
local valid = true
|
||||||
|
@ -240,12 +310,50 @@ local function update_suggestions()
|
||||||
map = autocomplete.map_manually
|
map = autocomplete.map_manually
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local assigned_sym = {}
|
||||||
|
|
||||||
-- get all relevant suggestions for given filename
|
-- get all relevant suggestions for given filename
|
||||||
local items = {}
|
local items = {}
|
||||||
for _, v in pairs(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)
|
||||||
|
assigned_sym[item.text] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Append the global, local or related text symbols if applicable
|
||||||
|
local scope = config.plugins.autocomplete.suggestions_scope
|
||||||
|
|
||||||
|
if not triggered_manually then
|
||||||
|
local text_symbols = nil
|
||||||
|
|
||||||
|
if scope == "global" then
|
||||||
|
text_symbols = global_symbols
|
||||||
|
elseif scope == "local" and cache[doc] and cache[doc].symbols then
|
||||||
|
text_symbols = cache[doc].symbols
|
||||||
|
elseif scope == "related" then
|
||||||
|
for _, d in ipairs(core.docs) do
|
||||||
|
if doc.syntax == d.syntax then
|
||||||
|
if cache[d].symbols then
|
||||||
|
for name in pairs(cache[d].symbols) do
|
||||||
|
if not assigned_sym[name] then
|
||||||
|
table.insert(items, setmetatable(
|
||||||
|
{text = name, info = "normal"}, mt
|
||||||
|
))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if text_symbols then
|
||||||
|
for name in pairs(text_symbols) do
|
||||||
|
if not assigned_sym[name] then
|
||||||
|
table.insert(items, setmetatable({text = name, info = "normal"}, mt))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -286,13 +394,23 @@ local function get_suggestions_rect(av)
|
||||||
y = y + av:get_line_height() + style.padding.y
|
y = y + av:get_line_height() + style.padding.y
|
||||||
local font = av:get_font()
|
local font = av:get_font()
|
||||||
local th = font:get_height()
|
local th = font:get_height()
|
||||||
|
local has_icons = false
|
||||||
|
local hide_info = config.plugins.autocomplete.hide_info
|
||||||
|
local hide_icons = config.plugins.autocomplete.hide_icons
|
||||||
|
|
||||||
local max_width = 0
|
local max_width = 0
|
||||||
for _, s in ipairs(suggestions) do
|
for _, s in ipairs(suggestions) do
|
||||||
local w = font:get_width(s.text)
|
local w = font:get_width(s.text)
|
||||||
if s.info then
|
if s.info and not hide_info then
|
||||||
w = w + style.font:get_width(s.info) + style.padding.x
|
w = w + style.font:get_width(s.info) + style.padding.x
|
||||||
end
|
end
|
||||||
|
local icon = s.icon or s.info
|
||||||
|
if not hide_icons and icon and autocomplete.icons[icon] then
|
||||||
|
w = w + autocomplete.icons[icon].font:get_width(
|
||||||
|
autocomplete.icons[icon].char
|
||||||
|
) + (style.padding.x / 2)
|
||||||
|
has_icons = true
|
||||||
|
end
|
||||||
max_width = math.max(max_width, w)
|
max_width = math.max(max_width, w)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -319,7 +437,8 @@ local function get_suggestions_rect(av)
|
||||||
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,
|
||||||
max_items * (th + style.padding.y) + style.padding.y
|
max_items * (th + style.padding.y) + style.padding.y,
|
||||||
|
has_icons
|
||||||
end
|
end
|
||||||
|
|
||||||
local function wrap_line(line, max_chars)
|
local function wrap_line(line, max_chars)
|
||||||
|
@ -439,7 +558,7 @@ local function draw_suggestions_box(av)
|
||||||
local ah = config.plugins.autocomplete.max_height
|
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, has_icons = get_suggestions_rect(av)
|
||||||
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
||||||
|
|
||||||
-- draw text
|
-- draw text
|
||||||
|
@ -448,17 +567,52 @@ local function draw_suggestions_box(av)
|
||||||
local y = ry + style.padding.y / 2
|
local y = ry + style.padding.y / 2
|
||||||
local show_count = #suggestions <= ah and #suggestions or ah
|
local show_count = #suggestions <= ah and #suggestions or ah
|
||||||
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
|
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
|
||||||
|
local hide_info = config.plugins.autocomplete.hide_info
|
||||||
|
|
||||||
for i=start_index, start_index+show_count-1, 1 do
|
for i=start_index, start_index+show_count-1, 1 do
|
||||||
if not suggestions[i] then
|
if not suggestions[i] then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
local s = suggestions[i]
|
local s = suggestions[i]
|
||||||
|
|
||||||
|
local icon_l_padding, icon_r_padding = 0, 0
|
||||||
|
|
||||||
|
if has_icons then
|
||||||
|
local icon = s.icon or s.info
|
||||||
|
if icon and autocomplete.icons[icon] then
|
||||||
|
local ifont = autocomplete.icons[icon].font
|
||||||
|
local itext = autocomplete.icons[icon].char
|
||||||
|
local icolor = autocomplete.icons[icon].color
|
||||||
|
if i == suggestions_idx then
|
||||||
|
icolor = style.accent
|
||||||
|
elseif type(icolor) == "string" then
|
||||||
|
icolor = style.syntax[icolor]
|
||||||
|
end
|
||||||
|
if config.plugins.autocomplete.icon_position == "left" then
|
||||||
|
common.draw_text(
|
||||||
|
ifont, icolor, itext, "left", rx + style.padding.x, y, rw, lh
|
||||||
|
)
|
||||||
|
icon_l_padding = ifont:get_width(itext) + (style.padding.x / 2)
|
||||||
|
else
|
||||||
|
common.draw_text(
|
||||||
|
ifont, icolor, itext, "right", rx, y, rw - style.padding.x, lh
|
||||||
|
)
|
||||||
|
icon_r_padding = ifont:get_width(itext) + (style.padding.x / 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
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(
|
||||||
if s.info then
|
font, color, s.text, "left",
|
||||||
|
rx + icon_l_padding + style.padding.x, y, rw, lh
|
||||||
|
)
|
||||||
|
if s.info and not hide_info then
|
||||||
color = (i == suggestions_idx) and style.text or style.dim
|
color = (i == suggestions_idx) and style.text or style.dim
|
||||||
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 - icon_r_padding - style.padding.x, lh
|
||||||
|
)
|
||||||
end
|
end
|
||||||
y = y + lh
|
y = y + lh
|
||||||
if suggestions_idx == i then
|
if suggestions_idx == i then
|
||||||
|
@ -619,6 +773,31 @@ function autocomplete.can_complete()
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Register a font icon that can be assigned to completion items.
|
||||||
|
---@param name string
|
||||||
|
---@param character string
|
||||||
|
---@param font? renderer.font
|
||||||
|
---@param color? string | renderer.color A style.syntax[] name or specific color
|
||||||
|
function autocomplete.add_icon(name, character, font, color)
|
||||||
|
local color_type = type(color)
|
||||||
|
assert(
|
||||||
|
not color or color_type == "table"
|
||||||
|
or (color_type == "string" and style.syntax[color]),
|
||||||
|
"invalid icon color given"
|
||||||
|
)
|
||||||
|
autocomplete.icons[name] = {
|
||||||
|
char = character,
|
||||||
|
font = font or style.code_font,
|
||||||
|
color = color or "keyword"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Register built-in syntax symbol types icon
|
||||||
|
--
|
||||||
|
for name, _ in pairs(style.syntax) do
|
||||||
|
autocomplete.add_icon(name, "M", style.icon_font, name)
|
||||||
|
end
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Commands
|
-- Commands
|
||||||
|
@ -632,7 +811,6 @@ command.add(predicate, {
|
||||||
["autocomplete:complete"] = function(dv)
|
["autocomplete:complete"] = function(dv)
|
||||||
local doc = dv.doc
|
local doc = dv.doc
|
||||||
local item = suggestions[suggestions_idx]
|
local item = suggestions[suggestions_idx]
|
||||||
local text = item.text
|
|
||||||
local inserted = false
|
local inserted = false
|
||||||
if item.onselect then
|
if item.onselect then
|
||||||
inserted = item.onselect(suggestions_idx, item)
|
inserted = item.onselect(suggestions_idx, item)
|
||||||
|
|
Loading…
Reference in New Issue