From 27f24701c4ebf084a8b7cc9a42d13a22932ebbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jefferson=20Gonz=C3=A1lez?= Date: Sat, 26 Aug 2023 10:50:48 -0400 Subject: [PATCH] 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 --- data/plugins/autocomplete.lua | 218 ++++++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 20 deletions(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index cf228b6e..98b2dcd0 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -10,6 +10,10 @@ local RootView = require "core.rootview" local DocView = require "core.docview" local Doc = require "core.doc" +---Symbols cache of all open documents +---@type table +local cache = setmetatable({}, { __mode = "k" }) + config.plugins.autocomplete = common.merge({ -- Amount of characters that need to be written for autocomplete min_len = 3, @@ -19,8 +23,16 @@ config.plugins.autocomplete = common.merge({ max_suggestions = 100, -- Maximum amount of symbols to cache per document max_symbols = 4000, + -- Which symbols to show on the suggestions list: global, local, related, none + suggestions_scope = "global", -- Font size of the description box 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 config_spec = { name = "Autocomplete", @@ -60,6 +72,26 @@ config.plugins.autocomplete = common.merge({ min = 1000, 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", description = "Font size of the description box.", @@ -67,6 +99,31 @@ config.plugins.autocomplete = common.merge({ type = "number", default = 12, 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) @@ -76,6 +133,7 @@ local autocomplete = {} autocomplete.map = {} autocomplete.map_manually = {} autocomplete.on_close = nil +autocomplete.icons = {} -- Flag that indicates if the autocomplete box was manually triggered -- with the autocomplete.complete() function to prevent the suggestions @@ -95,6 +153,7 @@ function autocomplete.add(t, manually_triggered) { text = text, info = info.info, + icon = info.icon, -- Name of icon to show desc = info.desc, -- Description shown on item selected onhover = info.onhover, -- A callback called once when item is hovered onselect = info.onselect, -- A callback called when item is selected @@ -119,28 +178,35 @@ end -- -- Thread that scans open document symbols and cache them -- -local max_symbols = config.plugins.autocomplete.max_symbols +local global_symbols = {} core.add_thread(function() - local cache = setmetatable({}, { __mode = "k" }) - - local function get_syntax_symbols(symbols, doc) - if doc.syntax then - for sym in pairs(doc.syntax.symbols) do - symbols[sym] = true + local function load_syntax_symbols(doc) + if doc.syntax and not autocomplete.map["language_"..doc.syntax.name] then + local symbols = { + name = "language_"..doc.syntax.name, + files = doc.syntax.files, + items = {} + } + for name, type in pairs(doc.syntax.symbols) do + symbols.items[name] = type end + autocomplete.add(symbols) + return symbols.items end + return {} end local function get_symbols(doc) 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 local i = 1 local symbols_count = 0 while i <= #doc.lines 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 if symbols_count > max_symbols then s = nil @@ -186,14 +252,18 @@ core.add_thread(function() } end -- update symbol set with doc's symbol set - for sym in pairs(cache[doc].symbols) do - symbols[sym] = true + if config.plugins.autocomplete.suggestions_scope == "global" then + for sym in pairs(cache[doc].symbols) do + symbols[sym] = true + end end coroutine.yield() end - -- update symbols list - autocomplete.add { name = "open-docs", items = symbols } + -- update global symbols list + if config.plugins.autocomplete.suggestions_scope == "global" then + global_symbols = symbols + end -- wait for next scan local valid = true @@ -240,12 +310,50 @@ local function update_suggestions() map = autocomplete.map_manually end + local assigned_sym = {} + -- get all relevant suggestions for given filename local items = {} 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) + 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 @@ -286,13 +394,23 @@ local function get_suggestions_rect(av) y = y + av:get_line_height() + style.padding.y local font = av:get_font() 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 for _, s in ipairs(suggestions) do 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 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) end @@ -319,7 +437,8 @@ local function get_suggestions_rect(av) x - style.padding.x, y - style.padding.y, 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 local function wrap_line(line, max_chars) @@ -439,7 +558,7 @@ local function draw_suggestions_box(av) local ah = config.plugins.autocomplete.max_height -- 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) -- draw text @@ -448,17 +567,52 @@ local function draw_suggestions_box(av) local y = ry + style.padding.y / 2 local show_count = #suggestions <= ah and #suggestions or ah 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 if not suggestions[i] then break end 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 - common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) - if s.info then + common.draw_text( + 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 - 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 y = y + lh if suggestions_idx == i then @@ -619,6 +773,31 @@ function autocomplete.can_complete() return false 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 @@ -632,7 +811,6 @@ command.add(predicate, { ["autocomplete:complete"] = function(dv) local doc = dv.doc local item = suggestions[suggestions_idx] - local text = item.text local inserted = false if item.onselect then inserted = item.onselect(suggestions_idx, item)