autocomplete.lua 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. local core = require "core"
  2. local common = require "core.common"
  3. local config = require "core.config"
  4. local command = require "core.command"
  5. local style = require "core.style"
  6. local keymap = require "core.keymap"
  7. local translate = require "core.doc.translate"
  8. local RootView = require "core.rootview"
  9. local DocView = require "core.docview"
  10. local max_suggestions = 6
  11. local symbols = {}
  12. core.add_thread(function()
  13. local cache = setmetatable({}, { __mode = "k" })
  14. local function get_symbols(doc)
  15. local i = 1
  16. local s = {}
  17. while i < #doc.lines do
  18. for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
  19. s[sym] = true
  20. end
  21. i = i + 1
  22. if i % 100 == 0 then coroutine.yield() end
  23. end
  24. return s
  25. end
  26. local function cache_is_valid(doc)
  27. local c = cache[doc]
  28. return c and c.last_change_id == doc:get_change_id()
  29. end
  30. while true do
  31. -- lift all symbols from all docs
  32. local t = {}
  33. for _, doc in ipairs(core.docs) do
  34. -- update the cache if the doc has changed since the last iteration
  35. if not cache_is_valid(doc) then
  36. cache[doc] = {
  37. last_change_id = doc:get_change_id(),
  38. symbols = get_symbols(doc)
  39. }
  40. end
  41. -- update symbol set with doc's symbol set
  42. for sym in pairs(cache[doc].symbols) do
  43. t[sym] = true
  44. end
  45. coroutine.yield()
  46. end
  47. -- update symbols list
  48. symbols = {}
  49. for sym in pairs(t) do
  50. table.insert(symbols, sym)
  51. end
  52. -- wait for next scan
  53. local valid = true
  54. while valid do
  55. coroutine.yield(1)
  56. for _, doc in ipairs(core.docs) do
  57. if not cache_is_valid(doc) then
  58. valid = false
  59. end
  60. end
  61. end
  62. end
  63. end)
  64. local partial = ""
  65. local suggestions_idx = 1
  66. local suggestions = {}
  67. local last_active_view
  68. local last_line, last_col
  69. local function reset_suggestions()
  70. suggestions_idx = 1
  71. suggestions = {}
  72. end
  73. local function get_partial_symbol()
  74. local doc = core.active_view.doc
  75. local line2, col2 = doc:get_selection()
  76. local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
  77. return doc:get_text(line1, col1, line2, col2)
  78. end
  79. local function get_active_view()
  80. if getmetatable(core.active_view) == DocView then
  81. last_active_view = core.active_view
  82. return core.active_view
  83. end
  84. end
  85. local function get_suggestions_rect(av)
  86. if #suggestions == 0 then
  87. return 0, 0, 0, 0
  88. end
  89. local line, col = av.doc:get_selection()
  90. local x, y = av:get_line_screen_position(line)
  91. x = x + av:get_col_x_offset(line, col - #partial)
  92. y = y + av:get_line_height() + style.padding.y
  93. local font = av:get_font()
  94. local th = font:get_height()
  95. local max_width = 0
  96. for i, sym in ipairs(suggestions) do
  97. max_width = math.max(max_width, font:get_width(sym))
  98. end
  99. return
  100. x - style.padding.x,
  101. y - style.padding.y,
  102. max_width + style.padding.x * 2,
  103. #suggestions * (th + style.padding.y) + style.padding.y
  104. end
  105. local function draw_suggestions_box(av)
  106. -- draw background rect
  107. local rx, ry, rw, rh = get_suggestions_rect(av)
  108. renderer.draw_rect(rx, ry, rw, rh, style.background3)
  109. -- draw text
  110. local font = av:get_font()
  111. local th = font:get_height()
  112. local x, y = rx + style.padding.x, ry + style.padding.y
  113. for i, sym in ipairs(suggestions) do
  114. local color = (i == suggestions_idx) and style.accent or style.text
  115. renderer.draw_text(font, sym, x, y, color)
  116. y = y + th + style.padding.y
  117. end
  118. end
  119. -- patch event logic into RootView
  120. local on_text_input = RootView.on_text_input
  121. local update = RootView.update
  122. local draw = RootView.draw
  123. RootView.on_text_input = function(...)
  124. on_text_input(...)
  125. local av = get_active_view()
  126. if av then
  127. -- update partial symbol and suggestions
  128. partial = get_partial_symbol()
  129. if #partial >= 3 then
  130. local t = common.fuzzy_match(symbols, partial)
  131. for i = 1, max_suggestions do
  132. suggestions[i] = t[i]
  133. end
  134. last_line, last_col = av.doc:get_selection()
  135. else
  136. reset_suggestions()
  137. end
  138. -- scroll if rect is out of bounds of view
  139. local _, y, _, h = get_suggestions_rect(av)
  140. local limit = av.position.y + av.size.y
  141. if y + h > limit then
  142. av.scroll.to.y = av.scroll.y + y + h - limit
  143. end
  144. end
  145. end
  146. RootView.update = function(...)
  147. update(...)
  148. local av = get_active_view()
  149. if av then
  150. -- reset suggestions if caret was moved
  151. local line, col = av.doc:get_selection()
  152. if line ~= last_line or col ~= last_col then
  153. reset_suggestions()
  154. end
  155. end
  156. end
  157. RootView.draw = function(...)
  158. draw(...)
  159. local av = get_active_view()
  160. if av then
  161. -- draw suggestions box after everything else
  162. core.root_view:defer_draw(draw_suggestions_box, av)
  163. end
  164. end
  165. local function predicate()
  166. return get_active_view() and #suggestions > 0
  167. end
  168. command.add(predicate, {
  169. ["autocomplete:complete"] = function()
  170. local doc = core.active_view.doc
  171. local line, col = doc:get_selection()
  172. local text = suggestions[suggestions_idx]
  173. doc:insert(line, col, text)
  174. doc:remove(line, col, line, col - #partial)
  175. doc:set_selection(line, col + #text - #partial)
  176. reset_suggestions()
  177. end,
  178. ["autocomplete:previous"] = function()
  179. suggestions_idx = math.max(suggestions_idx - 1, 1)
  180. end,
  181. ["autocomplete:next"] = function()
  182. suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
  183. end,
  184. ["autocomplete:cancel"] = function()
  185. reset_suggestions()
  186. end,
  187. })
  188. keymap.add {
  189. ["tab"] = "autocomplete:complete",
  190. ["up"] = "autocomplete:previous",
  191. ["down"] = "autocomplete:next",
  192. ["escape"] = "autocomplete:cancel",
  193. }