aboutsummaryrefslogtreecommitdiff
path: root/data/plugins/autocomplete.lua
diff options
context:
space:
mode:
Diffstat (limited to 'data/plugins/autocomplete.lua')
-rw-r--r--data/plugins/autocomplete.lua240
1 files changed, 240 insertions, 0 deletions
diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua
new file mode 100644
index 00000000..6414e858
--- /dev/null
+++ b/data/plugins/autocomplete.lua
@@ -0,0 +1,240 @@
+local core = require "core"
+local common = require "core.common"
+local config = require "core.config"
+local command = require "core.command"
+local style = require "core.style"
+local keymap = require "core.keymap"
+local translate = require "core.doc.translate"
+local RootView = require "core.rootview"
+local DocView = require "core.docview"
+
+local max_suggestions = 6
+
+local symbols = {}
+
+
+core.add_thread(function()
+ local cache = setmetatable({}, { __mode = "k" })
+
+ local function get_symbols(doc)
+ local i = 1
+ local s = {}
+ while i < #doc.lines do
+ for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
+ s[sym] = true
+ end
+ i = i + 1
+ if i % 100 == 0 then coroutine.yield() end
+ end
+ return s
+ end
+
+ local function cache_is_valid(doc)
+ local c = cache[doc]
+ return c and c.last_change_id == doc:get_change_id()
+ end
+
+ while true do
+ -- lift all symbols from all docs
+ local t = {}
+ for _, doc in ipairs(core.docs) do
+ -- update the cache if the doc has changed since the last iteration
+ if not cache_is_valid(doc) then
+ cache[doc] = {
+ last_change_id = doc:get_change_id(),
+ symbols = get_symbols(doc)
+ }
+ end
+ -- update symbol set with doc's symbol set
+ for sym in pairs(cache[doc].symbols) do
+ t[sym] = true
+ end
+ coroutine.yield()
+ end
+
+ -- update symbols list
+ symbols = {}
+ for sym in pairs(t) do
+ table.insert(symbols, sym)
+ end
+
+ -- wait for next scan
+ local valid = true
+ while valid do
+ coroutine.yield(1)
+ for _, doc in ipairs(core.docs) do
+ if not cache_is_valid(doc) then
+ valid = false
+ end
+ end
+ end
+
+ end
+end)
+
+
+local partial = ""
+local suggestions_idx = 1
+local suggestions = {}
+local last_active_view
+local last_line, last_col
+
+
+local function reset_suggestions()
+ suggestions_idx = 1
+ suggestions = {}
+end
+
+
+local function get_partial_symbol()
+ local doc = core.active_view.doc
+ local line2, col2 = doc:get_selection()
+ local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
+ return doc:get_text(line1, col1, line2, col2)
+end
+
+
+local function get_active_view()
+ if getmetatable(core.active_view) == DocView then
+ last_active_view = core.active_view
+ return core.active_view
+ end
+end
+
+
+local function get_suggestions_rect(av)
+ if #suggestions == 0 then
+ return 0, 0, 0, 0
+ end
+
+ local line, col = av.doc:get_selection()
+ local x, y = av:get_line_screen_position(line)
+ x = x + av:get_col_x_offset(line, col - #partial)
+ y = y + av:get_line_height() + style.padding.y
+ local font = av:get_font()
+ local th = font:get_height()
+
+ local max_width = 0
+ for i, sym in ipairs(suggestions) do
+ max_width = math.max(max_width, font:get_width(sym))
+ end
+
+ return
+ x - style.padding.x,
+ y - style.padding.y,
+ max_width + style.padding.x * 2,
+ #suggestions * (th + style.padding.y) + style.padding.y
+end
+
+
+local function draw_suggestions_box(av)
+ -- draw background rect
+ local rx, ry, rw, rh = get_suggestions_rect(av)
+ renderer.draw_rect(rx, ry, rw, rh, style.background3)
+
+ -- draw text
+ local font = av:get_font()
+ local th = font:get_height()
+ local x, y = rx + style.padding.x, ry + style.padding.y
+ for i, sym in ipairs(suggestions) do
+ local color = (i == suggestions_idx) and style.accent or style.text
+ renderer.draw_text(font, sym, x, y, color)
+ y = y + th + style.padding.y
+ end
+end
+
+
+-- 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()
+ if av then
+ -- update partial symbol and suggestions
+ partial = get_partial_symbol()
+ if #partial >= 3 then
+ local t = common.fuzzy_match(symbols, partial)
+ for i = 1, max_suggestions do
+ suggestions[i] = t[i]
+ end
+ last_line, last_col = av.doc:get_selection()
+ else
+ reset_suggestions()
+ end
+
+ -- scroll if rect is out of bounds of view
+ local _, y, _, h = get_suggestions_rect(av)
+ local limit = av.position.y + av.size.y
+ if y + h > limit then
+ av.scroll.to.y = av.scroll.y + y + h - limit
+ end
+ end
+end
+
+
+RootView.update = function(...)
+ update(...)
+
+ local av = get_active_view()
+ if av then
+ -- reset suggestions if caret was moved
+ local line, col = av.doc:get_selection()
+ if line ~= last_line or col ~= last_col then
+ reset_suggestions()
+ end
+ end
+end
+
+
+RootView.draw = function(...)
+ draw(...)
+
+ local av = get_active_view()
+ if av then
+ -- draw suggestions box after everything else
+ core.root_view:defer_draw(draw_suggestions_box, av)
+ end
+end
+
+
+local function predicate()
+ return get_active_view() and #suggestions > 0
+end
+
+
+command.add(predicate, {
+ ["autocomplete:complete"] = function()
+ local doc = core.active_view.doc
+ local line, col = doc:get_selection()
+ local text = suggestions[suggestions_idx]
+ doc:insert(line, col, text)
+ doc:remove(line, col, line, col - #partial)
+ doc:set_selection(line, col + #text - #partial)
+ reset_suggestions()
+ end,
+
+ ["autocomplete:previous"] = function()
+ suggestions_idx = math.max(suggestions_idx - 1, 1)
+ end,
+
+ ["autocomplete:next"] = function()
+ suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
+ end,
+
+ ["autocomplete:cancel"] = function()
+ reset_suggestions()
+ end,
+})
+
+
+keymap.add {
+ ["tab"] = "autocomplete:complete",
+ ["up"] = "autocomplete:previous",
+ ["down"] = "autocomplete:next",
+ ["escape"] = "autocomplete:cancel",
+}