From 685956cbdb1dd4e1b792e6034b94920d90e089e5 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 11 Jun 2022 06:21:55 +0200 Subject: [PATCH 1/3] Add `Highlighter:update_notify` to keep track of retokenized lines This is helpful for plugins that need to know when a line has been retokenized. --- data/core/doc/highlighter.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index 9ba7b634..888c82aa 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -22,13 +22,21 @@ function Highlighter:new(doc) else local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) + local retokenized_from for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] if not (line and line.init_state == state and line.text == self.doc.lines[i]) then + retokenized_from = retokenized_from or i self.lines[i] = self:tokenize_line(i, state) + elseif retokenized_from then + self:update_notify(retokenized_from, i - retokenized_from - 1) + retokenized_from = nil end end + if retokenized_from then + self:update_notify(retokenized_from, max - retokenized_from) + end self.first_invalid_line = max + 1 core.redraw = true @@ -71,6 +79,10 @@ function Highlighter:remove_notify(line, n) common.splice(self.lines, line, n) end +function Highlighter:update_notify(line, n) + -- plugins can hook here to be notified that lines have been retokenized +end + function Highlighter:tokenize_line(idx, state) local res = {} @@ -87,6 +99,7 @@ function Highlighter:get_line(idx) local prev = self.lines[idx - 1] line = self:tokenize_line(idx, prev and prev.state) self.lines[idx] = line + self:update_notify(idx, 0) end self.max_wanted_line = math.max(self.max_wanted_line, idx) return line From f38723ea4698bf74b174625aba01731315123854 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 11 Jun 2022 06:30:13 +0200 Subject: [PATCH 2/3] `drawwhitespace`: Cache whitespace location --- data/plugins/drawwhitespace.lua | 174 +++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 47 deletions(-) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 6186eac6..46709e20 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -4,6 +4,7 @@ local style = require "core.style" local DocView = require "core.docview" local common = require "core.common" local config = require "core.config" +local Highlighter = require "core.doc.highlighter" config.plugins.drawwhitespace = common.merge({ enabled = true, @@ -95,6 +96,52 @@ config.plugins.drawwhitespace = common.merge({ } }, config.plugins.drawwhitespace) + +local ws_cache = setmetatable({}, { __mode = "k" }) + +-- Move cache to make space for new lines +local prev_insert_notify = Highlighter.insert_notify +function Highlighter:insert_notify(line, n, ...) + prev_insert_notify(self, line, n, ...) + local blanks = { } + if not ws_cache[self] then + ws_cache[self] = {} + end + local to = math.min(line + n, #self.doc.lines) + for i=#self.doc.lines+n,to,-1 do + ws_cache[self][i] = ws_cache[self][i - n] + end + for i=line,to do + ws_cache[self][i] = nil + end +end + +-- Close the cache gap created by removed lines +local prev_remove_notify = Highlighter.remove_notify +function Highlighter:remove_notify(line, n, ...) + prev_remove_notify(self, line, n, ...) + if not ws_cache[self] then + ws_cache[self] = {} + end + local to = math.max(line + n, #self.doc.lines) + for i=line,to do + ws_cache[self][i] = ws_cache[self][i + n] + end +end + +-- Remove changed lines from the cache +local prev_update_notify = Highlighter.update_notify +function Highlighter:update_notify(line, n, ...) + prev_update_notify(self, line, n, ...) + if not ws_cache[self] then + ws_cache[self] = {} + end + for i=line,line+n do + ws_cache[self][i] = nil + end +end + + local function get_option(substitution, option) if substitution[option] == nil then return config.plugins.drawwhitespace[option] @@ -114,66 +161,99 @@ function DocView:draw_line_text(idx, x, y) local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"]) local ty = y + self:get_line_text_y_offset() - local tx - local text, offset, s, e = self.doc.lines[idx], 1 - local x1, _, x2, _ = self:get_content_bounds() - local _offset = self:get_x_offset_col(idx, x1) - for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do - local char = substitution.char - local sub = substitution.sub - offset = _offset + if + not ws_cache[self.doc.highlighter] + or ws_cache[self.doc.highlighter].font ~= font + then + ws_cache[self.doc.highlighter] = {font = font} + end - local show_leading = get_option(substitution, "show_leading") - local show_middle = get_option(substitution, "show_middle") - local show_trailing = get_option(substitution, "show_trailing") + if not ws_cache[self.doc.highlighter][idx] then -- need to cache line + local cache = {} - local show_middle_min = get_option(substitution, "show_middle_min") + local tx + local text = self.doc.lines[idx] - local base_color = get_option(substitution, "color") - local leading_color = get_option(substitution, "leading_color") or base_color - local middle_color = get_option(substitution, "middle_color") or base_color - local trailing_color = get_option(substitution, "trailing_color") or base_color + for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do + local char = substitution.char + local sub = substitution.sub + local offset = 1 - local pattern = char.."+" - while true do - s, e = text:find(pattern, offset) - if not s then break end + local show_leading = get_option(substitution, "show_leading") + local show_middle = get_option(substitution, "show_middle") + local show_trailing = get_option(substitution, "show_trailing") - tx = self:get_col_x_offset(idx, s) + x + local show_middle_min = get_option(substitution, "show_middle_min") - local color = base_color - local draw = false + local base_color = get_option(substitution, "color") + local leading_color = get_option(substitution, "leading_color") or base_color + local middle_color = get_option(substitution, "middle_color") or base_color + local trailing_color = get_option(substitution, "trailing_color") or base_color - if e == #text - 1 then - draw = show_trailing - color = trailing_color - elseif s == 1 then - draw = show_leading - color = leading_color - else - draw = show_middle and (e - s + 1 >= show_middle_min) - color = middle_color - end + local pattern = char.."+" + while true do + local s, e = text:find(pattern, offset) + if not s then break end - if draw then - -- We need to draw tabs one at a time because they might have a - -- different size than the substituting character. - -- This also applies to any other char if we use non-monospace fonts - -- but we ignore this case for now. - if char == "\t" then - for i = s,e do - tx = self:get_col_x_offset(idx, i) + x - tx = renderer.draw_text(font, sub, tx, ty, color) - end + tx = self:get_col_x_offset(idx, s) + + local color = base_color + local draw = false + + if e == #text - 1 then + draw = show_trailing + color = trailing_color + elseif s == 1 then + draw = show_leading + color = leading_color else - tx = renderer.draw_text(font, string.rep(sub, e - s + 1), tx, ty, color) + draw = show_middle and (e - s + 1 >= show_middle_min) + color = middle_color end - end - if tx > x + x2 then break end - offset = e + 1 + if draw then + local last_cache_idx = #cache + -- We need to draw tabs one at a time because they might have a + -- different size than the substituting character. + -- This also applies to any other char if we use non-monospace fonts + -- but we ignore this case for now. + if char == "\t" then + for i = s,e do + tx = self:get_col_x_offset(idx, i) + cache[last_cache_idx + 1] = sub + cache[last_cache_idx + 2] = tx + cache[last_cache_idx + 3] = font:get_width(sub) + cache[last_cache_idx + 4] = color + last_cache_idx = last_cache_idx + 4 + end + else + cache[last_cache_idx + 1] = string.rep(sub, e - s + 1) + cache[last_cache_idx + 2] = tx + cache[last_cache_idx + 3] = font:get_width(cache[last_cache_idx + 1]) + cache[last_cache_idx + 4] = color + end + end + offset = e + 1 + end end + ws_cache[self.doc.highlighter][idx] = cache + end + + -- draw from cache + local x1, _, x2, _ = self:get_content_bounds() + x1 = x1 + x + x2 = x2 + x + local cache = ws_cache[self.doc.highlighter][idx] + for i=1,#cache,4 do + local sub = cache[i] + local tx = cache[i + 1] + x + local tw = cache[i + 2] + local color = cache[i + 3] + if tx + tw >= x1 then + tx = renderer.draw_text(font, sub, tx, y, color) + end + if tx > x2 then break end end return draw_line_text(self, idx, x, y) From 2d3abd2533b2e57e7f140f34a9a0807098adf68c Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 16 Jun 2022 00:03:25 +0200 Subject: [PATCH 3/3] `drawwhitespace`: Invalidate cache on config changes --- data/plugins/drawwhitespace.lua | 41 ++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 46709e20..ad84658d 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -97,13 +97,48 @@ config.plugins.drawwhitespace = common.merge({ }, config.plugins.drawwhitespace) -local ws_cache = setmetatable({}, { __mode = "k" }) +local ws_cache +local cached_settings +local function reset_cache() + ws_cache = setmetatable({}, { __mode = "k" }) + local settings = config.plugins.drawwhitespace + cached_settings = { + show_leading = settings.show_leading, + show_trailing = settings.show_trailing, + show_middle = settings.show_middle, + show_middle_min = settings.show_middle_min, + color = settings.color, + leading_color = settings.leading_color, + middle_color = settings.middle_color, + trailing_color = settings.trailing_color, + substitutions = settings.substitutions, + } +end +reset_cache() + +local function reset_cache_if_needed() + local settings = config.plugins.drawwhitespace + if + not ws_cache or + cached_settings.show_leading ~= settings.show_leading + or cached_settings.show_trailing ~= settings.show_trailing + or cached_settings.show_middle ~= settings.show_middle + or cached_settings.show_middle_min ~= settings.show_middle_min + or cached_settings.color ~= settings.color + or cached_settings.leading_color ~= settings.leading_color + or cached_settings.middle_color ~= settings.middle_color + or cached_settings.trailing_color ~= settings.trailing_color + -- we assume that the entire table changes + or cached_settings.substitutions ~= settings.substitutions + then + reset_cache() + end +end -- Move cache to make space for new lines local prev_insert_notify = Highlighter.insert_notify function Highlighter:insert_notify(line, n, ...) prev_insert_notify(self, line, n, ...) - local blanks = { } if not ws_cache[self] then ws_cache[self] = {} end @@ -160,8 +195,8 @@ function DocView:draw_line_text(idx, x, y) end local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"]) - local ty = y + self:get_line_text_y_offset() + reset_cache_if_needed() if not ws_cache[self.doc.highlighter] or ws_cache[self.doc.highlighter].font ~= font