Improvements and new features to autocomplete plugin in preparation for LSP plugin. (#235)
This commit is contained in:
parent
ae95b04f69
commit
e252c2f914
|
@ -8,25 +8,65 @@ local keymap = require "core.keymap"
|
||||||
local translate = require "core.doc.translate"
|
local translate = require "core.doc.translate"
|
||||||
local RootView = require "core.rootview"
|
local RootView = require "core.rootview"
|
||||||
local DocView = require "core.docview"
|
local DocView = require "core.docview"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
|
||||||
config.autocomplete_max_suggestions = 6
|
-- Amount of characters that need to be written for autocomplete
|
||||||
|
config.autocomplete_min_len = 1
|
||||||
|
-- The max amount of visible items
|
||||||
|
config.autocomplete_max_height = 6
|
||||||
|
-- The max amount of scrollable items
|
||||||
|
config.autocomplete_max_suggestions = 100
|
||||||
|
-- Maximum amount of symbols to cache per document
|
||||||
|
config.max_symbols = 2000
|
||||||
|
|
||||||
local autocomplete = {}
|
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 }
|
local mt = { __tostring = function(t) return t.text end }
|
||||||
|
|
||||||
function autocomplete.add(t)
|
function autocomplete.add(t, triggered_manually)
|
||||||
local items = {}
|
local items = {}
|
||||||
for text, info in pairs(t.items) do
|
for text, info in pairs(t.items) do
|
||||||
info = (type(info) == "string") and info
|
if type(info) == "table" then
|
||||||
table.insert(items, setmetatable({ text = text, info = info }, mt))
|
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
|
end
|
||||||
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
|
|
||||||
end
|
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()
|
core.add_thread(function()
|
||||||
local cache = setmetatable({}, { __mode = "k" })
|
local cache = setmetatable({}, { __mode = "k" })
|
||||||
|
@ -109,16 +149,39 @@ local last_line, last_col
|
||||||
local function reset_suggestions()
|
local function reset_suggestions()
|
||||||
suggestions_idx = 1
|
suggestions_idx = 1
|
||||||
suggestions = {}
|
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
|
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 function update_suggestions()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
local filename = doc and doc.filename or ""
|
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
|
-- get all relevant suggestions for given filename
|
||||||
local items = {}
|
local items = {}
|
||||||
for _, v in pairs(autocomplete.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)
|
||||||
|
@ -138,7 +201,6 @@ local function update_suggestions()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_partial_symbol()
|
local function get_partial_symbol()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
local line2, col2 = doc:get_selection()
|
local line2, col2 = doc:get_selection()
|
||||||
|
@ -146,14 +208,12 @@ local function get_partial_symbol()
|
||||||
return doc:get_text(line1, col1, line2, col2)
|
return doc:get_text(line1, col1, line2, col2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_active_view()
|
local function get_active_view()
|
||||||
if getmetatable(core.active_view) == DocView then
|
if getmetatable(core.active_view) == DocView then
|
||||||
return core.active_view
|
return core.active_view
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function get_suggestions_rect(av)
|
local function get_suggestions_rect(av)
|
||||||
if #suggestions == 0 then
|
if #suggestions == 0 then
|
||||||
return 0, 0, 0, 0
|
return 0, 0, 0, 0
|
||||||
|
@ -175,15 +235,67 @@ local function get_suggestions_rect(av)
|
||||||
max_width = math.max(max_width, w)
|
max_width = math.max(max_width, w)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local ah = config.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
|
return
|
||||||
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,
|
||||||
#suggestions * (th + style.padding.y) + style.padding.y
|
max_items * (th + style.padding.y) + style.padding.y
|
||||||
end
|
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)
|
local function draw_suggestions_box(av)
|
||||||
|
if #suggestions <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ah = config.autocomplete_max_height
|
||||||
|
|
||||||
-- draw background rect
|
-- draw background rect
|
||||||
local rx, ry, rw, rh = get_suggestions_rect(av)
|
local rx, ry, rw, rh = get_suggestions_rect(av)
|
||||||
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
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 font = av:get_font()
|
||||||
local lh = font:get_height() + style.padding.y
|
local lh = font:get_height() + style.padding.y
|
||||||
local y = ry + style.padding.y / 2
|
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
|
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(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
|
||||||
if s.info then
|
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)
|
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
|
||||||
end
|
end
|
||||||
y = y + lh
|
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
|
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
|
end
|
||||||
|
|
||||||
|
local function show_autocomplete()
|
||||||
-- 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 av = get_active_view()
|
local av = get_active_view()
|
||||||
if av then
|
if av then
|
||||||
-- update partial symbol and suggestions
|
-- update partial symbol and suggestions
|
||||||
partial = get_partial_symbol()
|
partial = get_partial_symbol()
|
||||||
if #partial >= 3 then
|
|
||||||
|
if #partial >= config.autocomplete_min_len or triggered_manually then
|
||||||
update_suggestions()
|
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
|
else
|
||||||
reset_suggestions()
|
reset_suggestions()
|
||||||
end
|
end
|
||||||
|
@ -233,6 +381,30 @@ RootView.on_text_input = function(...)
|
||||||
end
|
end
|
||||||
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(...)
|
RootView.update = function(...)
|
||||||
update(...)
|
update(...)
|
||||||
|
@ -241,13 +413,19 @@ RootView.update = function(...)
|
||||||
if av then
|
if av then
|
||||||
-- reset suggestions if caret was moved
|
-- reset suggestions if caret was moved
|
||||||
local line, col = av.doc:get_selection()
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
RootView.draw = function(...)
|
RootView.draw = function(...)
|
||||||
draw(...)
|
draw(...)
|
||||||
|
|
||||||
|
@ -258,12 +436,46 @@ RootView.draw = function(...)
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Commands
|
||||||
|
--
|
||||||
local function predicate()
|
local function predicate()
|
||||||
return get_active_view() and #suggestions > 0
|
return get_active_view() and #suggestions > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
command.add(predicate, {
|
command.add(predicate, {
|
||||||
["autocomplete:complete"] = function()
|
["autocomplete:complete"] = function()
|
||||||
local doc = core.active_view.doc
|
local doc = core.active_view.doc
|
||||||
|
@ -288,7 +500,9 @@ command.add(predicate, {
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Keymaps
|
||||||
|
--
|
||||||
keymap.add {
|
keymap.add {
|
||||||
["tab"] = "autocomplete:complete",
|
["tab"] = "autocomplete:complete",
|
||||||
["up"] = "autocomplete:previous",
|
["up"] = "autocomplete:previous",
|
||||||
|
|
Loading…
Reference in New Issue