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:
Jefferson González 2023-08-26 10:50:48 -04:00 committed by George Sokianos
parent 25a0943087
commit 27f24701c4
1 changed files with 198 additions and 20 deletions

View File

@ -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
for sym in pairs(cache[doc].symbols) do if config.plugins.autocomplete.suggestions_scope == "global" then
symbols[sym] = true for sym in pairs(cache[doc].symbols) do
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)