diff options
Diffstat (limited to 'plugins/minimap.lua')
-rw-r--r-- | plugins/minimap.lua | 960 |
1 files changed, 622 insertions, 338 deletions
diff --git a/plugins/minimap.lua b/plugins/minimap.lua index 4bf4fca..c2dd8f0 100644 --- a/plugins/minimap.lua +++ b/plugins/minimap.lua @@ -1,382 +1,666 @@ --- mod-version:2 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local Highlighter = require "core.doc.highlighter" local Object = require "core.object" +local Scrollbar = require "core.scrollbar" + +-- Sample configurations: +-- full width: +-- config.plugins.minimap.highlight_width = 100 +-- config.plugins.minimap.gutter_width = 0 +-- left side: +-- config.plugins.minimap.highlight_align = 'left' +-- config.plugins.minimap.highlight_width = 3 +-- config.plugins.minimap.gutter_width = 4 +-- right side: +-- config.plugins.minimap.highlight_align = 'right' +-- config.plugins.minimap.highlight_width = 5 +-- config.plugins.minimap.gutter_width = 0 -- General plugin settings -config.plugins.minimap = { - enabled = true, - width = 100, - instant_scroll = false, - syntax_highlight = true, - scale = 1, - -- how many spaces one tab is equivalent to - tab_width = 4, - draw_background = true, - - -- you can override these colors - selection_color = nil, - caret_color = nil, - - -- If other plugins provide per-line highlights, - -- this controls the placement. (e.g. gitdiff_highlight) - highlight_align = 'left', - highlight_width = 3, - gutter_width = 5, - -- try these values: - -- full width: - -- config.plugins.minimap.highlight_width = 100 - -- config.plugins.minimap.gutter_width = 0 - -- left side: - -- config.plugins.minimap.highlight_align = 'left' - -- config.plugins.minimap.highlight_width = 3 - -- config.plugins.minimap.gutter_width = 4 - -- right side: - -- config.plugins.minimap.highlight_align = 'right' - -- config.plugins.minimap.highlight_width = 5 - -- config.plugins.minimap.gutter_width = 0 +config.plugins.minimap = common.merge({ + enabled = true, + width = 100, + instant_scroll = false, + syntax_highlight = true, + scale = 1, + -- number of spaces needed to split a token + spaces_to_split = 2, + -- hide on small docs (can be true, false or min number of lines) + avoid_small_docs = false, + -- how many spaces one tab is equivalent to + tab_width = 4, + draw_background = true, + -- you can override these colors + selection_color = nil, + caret_color = nil, + -- If other plugins provide per-line highlights, + -- this controls the placement. (e.g. gitdiff_highlight) + highlight_align = 'left', + highlight_width = 3, + gutter_width = 5, + -- The config specification used by the settings gui + config_spec = { + name = "Mini Map", + { + label = "Enabled", + description = "Activate the minimap by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Width", + description = "Width of the minimap in pixels.", + path = "width", + type = "number", + default = 100, + min = 50, + max = 1000 + }, + { + label = "Instant Scroll", + description = "When enabled disables the scrolling animation.", + path = "instant_scroll", + type = "toggle", + default = false + }, + { + label = "Syntax Highlighting", + description = "Disable to improve performance.", + path = "syntax_highlight", + type = "toggle", + default = true + }, + { + label = "Scale", + description = "Size of the minimap using a scaling factor.", + path = "scale", + type = "number", + default = 1, + min = 0.5, + max = 10, + step = 0.1 + }, + { + label = "Spaces to split", + description = "Number of spaces needed to split a token.", + path = "spaces_to_split", + type = "number", + default = 2, + min = 1 + }, + { + label = "Hide for small Docs", + description = "Hide the minimap when a Doc is small enough.", + path = "avoid_small_docs", + type = "toggle", + default = false + }, + { + label = "Small Docs definition", + description = "Size of a Doc to be considered small. Use 0 to automatically decide.", + path = "avoid_small_docs_len", + type = "number", + default = 0, + min = 0, + on_apply = function(value) + if value == 0 then + config.plugins.minimap.avoid_small_docs = true + else + config.plugins.minimap.avoid_small_docs = value + end + end + }, + { + label = "Tabs Width", + description = "The amount of spaces that represent a tab.", + path = "tab_width", + type = "number", + default = 4, + min = 1, + max = 8 + }, + { + label = "Draw Background", + description = "When disabled makes the minimap transparent.", + path = "draw_background", + type = "toggle", + default = true + }, + { + label = "Selection Color", + description = "Background color of selected text in html notation eg: #FFFFFF. Leave empty to use default.", + path = "selection_color_html", + type = "string", + on_apply = function(value) + if value and value:match("#%x%x%x%x%x%x") then + config.plugins.minimap.selection_color = { common.color(value) } + else + config.plugins.minimap.selection_color = nil + end + end + }, + { + label = "Caret Color", + description = "Background color of active line in html notation eg: #FFFFFF. Leave empty to use default.", + path = "caret_color_html", + type = "string", + on_apply = function(value) + if value and value:match("#%x%x%x%x%x%x") then + config.plugins.minimap.caret_color = { common.color(value) } + else + config.plugins.minimap.caret_color = nil + end + end + }, + { + label = "Highlight Alignment", + path = "highlight_align", + type = "selection", + default = "left", + values = { + {"Left", "left"}, + {"Right", "right"} + } + }, + { + label = "Highlight Width", + path = "highlight_width", + type = "number", + default = 3, + min = 0, + max = 50 + }, + { + label = "Gutter Width", + description = "Left padding of the minimap.", + path = "gutter_width", + type = "number", + default = 5, + min = 0, + max = 50 + }, + } +}, config.plugins.minimap) + + +-- contains the settings values that require a cache reset if changed +local cached_settings = { + color_scheme_canary = nil, + syntax_highlight = nil, + spaces_to_split = nil, + scale = nil, + width = nil, } -- Configure size for rendering each char in the minimap -local char_height = 1 * SCALE * config.plugins.minimap.scale -local char_spacing = 0.8 * SCALE * config.plugins.minimap.scale -local line_spacing = 2 * SCALE * config.plugins.minimap.scale +local char_spacing +local char_height +local line_spacing + +-- cache for the location of the rects for each Doc +local highlighter_cache +local function reset_cache() + highlighter_cache = setmetatable({}, { __mode = "k" }) + cached_settings = { + color_scheme_canary = style.syntax["normal"], + syntax_highlight = config.plugins.minimap.syntax_highlight, + spaces_to_split = config.plugins.minimap.spaces_to_split, + scale = config.plugins.minimap.scale, + width = config.plugins.minimap.width, + } + char_spacing = 0.8 * SCALE * config.plugins.minimap.scale + -- keep y aligned to pixels + char_height = math.max(1, math.floor(1 * SCALE * config.plugins.minimap.scale + 0.5)) + line_spacing = math.max(1, math.floor(2 * SCALE * config.plugins.minimap.scale + 0.5)) +end +reset_cache() + + +local function reset_cache_if_needed() + if + cached_settings.color_scheme_canary ~= style.syntax["normal"] + or cached_settings.syntax_highlight ~= config.plugins.minimap.syntax_highlight + or cached_settings.spaces_to_split ~= config.plugins.minimap.spaces_to_split + or cached_settings.scale ~= config.plugins.minimap.scale + or cached_settings.width ~= config.plugins.minimap.width + 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 highlighter_cache[self] then + highlighter_cache[self] = {} + else + local to = math.min(line + n, #self.doc.lines) + for i=#self.doc.lines+n,to,-1 do + highlighter_cache[self][i] = highlighter_cache[self][i - n] + end + for i=line,to do + highlighter_cache[self][i] = nil + end + 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 highlighter_cache[self] then + highlighter_cache[self] = {} + else + local to = math.max(line + n, #self.doc.lines) + for i=line,to do + highlighter_cache[self][i] = highlighter_cache[self][i + n] + end + end +end + + +-- Remove changed lines from the cache +local prev_tokenize_line = Highlighter.tokenize_line +function Highlighter:tokenize_line(idx, state, ...) + local res = prev_tokenize_line(self, idx, state, ...) + if not highlighter_cache[self] then + highlighter_cache[self] = {} + end + highlighter_cache[self][idx] = nil + return res +end + +-- Ask the Highlighter to retokenize the lines we have in cache +local prev_invalidate = Highlighter.invalidate +function Highlighter:invalidate(idx, ...) + local cache = highlighter_cache[self] + if cache then + self.max_wanted_line = math.max(self.max_wanted_line, #cache) + end + return prev_invalidate(self, idx, ...) +end + + +-- Remove cache on Highlighter reset (for example on syntax change) +local prev_soft_reset = Highlighter.soft_reset +function Highlighter:soft_reset(...) + prev_soft_reset(self, ...) + highlighter_cache[self] = {} +end + + +local MiniMap = Scrollbar:extend() -local MiniMap = Object:extend() -function MiniMap:new() +function MiniMap:new(dv) + MiniMap.super.new(self, { direction = "v", alignment = "e" }) + self.dv = dv + self.enabled = nil end + function MiniMap:line_highlight_color(line_index) - -- other plugins can override this, and return a color + -- other plugins can override this, and return a color end -local minimap = MiniMap() -local function show_minimap() - return config.plugins.minimap.enabled - and getmetatable(core.active_view) == DocView - and core.active_view ~= core.command_view - and core.active_view.doc +function MiniMap:is_minimap_enabled() + if self.enabled ~= nil then return self.enabled end + if not config.plugins.minimap.enabled then return false end + if config.plugins.minimap.avoid_small_docs then + local last_line = #self.dv.doc.lines + if type(config.plugins.minimap.avoid_small_docs) == "number" then + return last_line > config.plugins.minimap.avoid_small_docs + else + local docview = self.dv + local _, y = docview:get_line_screen_position(last_line, docview.doc.lines[last_line]) + y = y + docview.scroll.y - docview.position.y + docview:get_line_height() + return y > docview.size.y + end + end + return true +end + + +function MiniMap:get_minimap_dimensions() + local x, y, w, h = self:get_track_rect() + local _, cy, _, cy2 = self.dv:get_content_bounds() + local lh = self.dv:get_line_height() + + local visible_lines_start = math.max(1, math.floor(cy / lh)) + local visible_lines_count = math.max(1, (cy2 - cy) / lh) + local minimap_lines_start = 1 + local minimap_lines_count = math.floor(h / line_spacing) + local line_count = #self.dv.doc.lines + + local is_file_too_large = line_count > 1 and line_count > minimap_lines_count + if is_file_too_large then + local scroll_pos = (visible_lines_start - 1) / + (line_count - visible_lines_count - 1) + scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled + + local thumb_height = visible_lines_count * line_spacing + local scroll_pos_pixels = scroll_pos * (h - thumb_height) + + minimap_lines_start = visible_lines_start - + math.floor(scroll_pos_pixels / line_spacing) + minimap_lines_start = math.max(1, minimap_lines_start) + end + return visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large end --- Overloaded since the default implementation adds a extra x3 size of hotspot for the mouse to hit the scrollbar. -local prev_scrollbar_overlaps_point = DocView.scrollbar_overlaps_point -DocView.scrollbar_overlaps_point = function(self, x, y) - if not show_minimap() then - return prev_scrollbar_overlaps_point(self, x, y) - end - local sx, sy, sw, sh = self:get_scrollbar_rect() - return x >= sx and x < sx + sw and y >= sy and y < sy + sh +function MiniMap:_get_track_rect_normal() + if not self:is_minimap_enabled() then return MiniMap.super._get_track_rect_normal(self) end + return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, self.dv.position.y, config.plugins.minimap.width, self.dv.size.y end --- Helper function to determine if current file is too large to be shown fully inside the minimap area. -local function is_file_too_large(self) - local line_count = #self.doc.lines - local _, _, _, sh = self:get_scrollbar_rect() - -- check if line count is too large to fit inside the minimap area - local max_minmap_lines = math.floor(sh / line_spacing) - return line_count > 1 and line_count > max_minmap_lines +function MiniMap:get_active_margin() if self:is_minimap_enabled() then return 0 else return MiniMap.super.get_active_margin(self) end end + + +function MiniMap:_get_thumb_rect_normal() + if not self:is_minimap_enabled() then return MiniMap.super._get_thumb_rect_normal(self) end + local visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local visible_y = self.dv.position.y + (visible_lines_start - 1) * line_spacing + if is_file_too_large then + local line_count = #self.dv.doc.lines + local scroll_pos = (visible_lines_start - 1) / + (line_count - visible_lines_count - 1) + scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled + + local thumb_height = visible_lines_count * line_spacing + local scroll_pos_pixels = scroll_pos * (self.dv.size.y - thumb_height) + visible_y = self.dv.position.y + scroll_pos_pixels + end + return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, visible_y, config.plugins.minimap.width, visible_lines_count * line_spacing end --- Overloaded with an extra check if the user clicked inside the minimap to automatically scroll to that line. -local prev_on_mouse_pressed = DocView.on_mouse_pressed -DocView.on_mouse_pressed = function(self, button, x, y, clicks) - if not show_minimap() then - return prev_on_mouse_pressed(self, button, x, y, clicks) - end - - -- check if user clicked in the minimap area and jump directly to that line - -- unless they are actually trying to perform a drag - local minimap_hit = self:scrollbar_overlaps_point(x, y) - if minimap_hit then - local line_count = #self.doc.lines - local minimap_height = line_count * line_spacing - - -- check if line count is too large to fit inside the minimap area - local is_too_large = is_file_too_large(self) - if is_too_large then - local _, _, _, sh = self:get_scrollbar_rect() - minimap_height = sh - end - - -- calc which line to jump to - local dy = y - self.position.y - local jump_to_line = math.floor((dy / minimap_height) * line_count) + 1 - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - - -- calc if user hit the currently visible area - local hit_visible_area = true - if is_too_large then - - local visible_height = visible_lines_count * line_spacing - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1 - local visible_y = self.position.y + scroll_pos * - (minimap_height - visible_height) - - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + visible_height * (1.0 - t) end - - if y < visible_y or y > visible_y + visible_height then - hit_visible_area = false - end - else - - -- If the click is on the currently visible line numbers, - -- ignore it since then they probably want to initiate a drag instead. - if jump_to_line < visible_lines_start or jump_to_line > visible_lines_start + - visible_lines_count then hit_visible_area = false end - end - - -- if user didn't click on the visible area (ie not dragging), scroll accordingly - if not hit_visible_area then - self:scroll_to_line(jump_to_line, false, config.plugins.minimap.instant_scroll) - end - - end - - return prev_on_mouse_pressed(self, button, x, y, clicks) + +function MiniMap:on_mouse_pressed(button, x, y, clicks) + local percent = MiniMap.super.on_mouse_pressed(self, button, x, y, clicks) + if not self:is_minimap_enabled() or not percent then return percent end + local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local _, _, w, h = self:get_track_rect() + local tx, ty, tw, th = self:get_thumb_rect() + if y >= ty and y < ty + th then self.drag_start_offset = (y - ty) - th / 2 return self.percent end + self.drag_start_offset = 0 + self.hovering.thumb = x >= tx and x < tx + tw and y >= ty and y < ty + th + self.dragging = self.hovering.thumb + local lh = self.dv:get_line_height() + percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) + return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) end --- Overloaded with pretty much the same logic as original DocView implementation, --- with the exception of the dragging scrollbar delta. We want it to behave a bit snappier --- since the "scrollbar" essentially represents the lines visible in the content view. -local prev_on_mouse_moved = DocView.on_mouse_moved -DocView.on_mouse_moved = function(self, x, y, dx, dy) - if not show_minimap() then - return prev_on_mouse_moved(self, x, y, dx, dy) - end - - if self.dragging_scrollbar then - local line_count = #self.doc.lines - local lh = self:get_line_height() - local delta = lh / line_spacing * dy - - if is_file_too_large(self) then - local _, sy, _, sh = self:get_scrollbar_rect() - delta = (line_count * lh) / sh * dy - end - - self.scroll.to.y = self.scroll.to.y + delta - end - - -- we need to "hide" that the scrollbar is dragging so that View doesnt does its own scrolling logic - local t = self.dragging_scrollbar - self.dragging_scrollbar = false - local r = prev_on_mouse_moved(self, x, y, dx, dy) - self.dragging_scrollbar = t - return r + +function MiniMap:on_mouse_moved(x, y, dx, dy) + local percent = MiniMap.super.on_mouse_moved(self, x, y, dx, dy) + if not self:is_minimap_enabled() or type(percent) ~= "number" then return percent end + local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local lh = self.dv:get_line_height() + local _, _, w, h = self:get_track_rect() + local tx, ty, tw, th = self:get_thumb_rect() + if x >= tx and x < tx + tw and y >= ty and y < ty + th then self.hovering.thumb = true end + if not self.hovering.thumb then return self.percent end + y = y - self.drag_start_offset + percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) + return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) end --- Overloaded since we want the mouse to interact with the full size of the minimap area, --- not juse the scrollbar. -local prev_get_scrollbar_rect = DocView.get_scrollbar_rect -DocView.get_scrollbar_rect = function(self) - if not show_minimap() then return prev_get_scrollbar_rect(self) end +function MiniMap:draw_thumb() + local color = self.hovering.thumb and style.scrollbar2 or style.scrollbar + local x, y, w, h = self:get_thumb_rect() + renderer.draw_rect(x, y, w, h, color) +end - return self.position.x + self.size.x - config.plugins.minimap.width * SCALE, - self.position.y, config.plugins.minimap.width * SCALE, self.size.y +function MiniMap:draw() + if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end + local dv = self.dv + local x, y, w, h = self:get_track_rect() + + local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar + local visual_color = highlight and style.scrollbar2 or style.scrollbar + + local visible_lines_start, visible_lines_count, + minimap_lines_start, minimap_lines_count = self:get_minimap_dimensions() + + if config.plugins.minimap.draw_background then + renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) + end + self:draw_thumb() + + -- highlight the selected lines, and the line with the caret on it + local selection_color = config.plugins.minimap.selection_color or style.dim + local caret_color = config.plugins.minimap.caret_color or style.caret + + for i, line1, col1, line2, col2 in dv.doc:get_selections() do + local selection1_y = y + (line1 - minimap_lines_start) * line_spacing + local selection2_y = y + (line2 - minimap_lines_start) * line_spacing + local selection_min_y = math.min(selection1_y, selection2_y) + local selection_h = math.abs(selection2_y - selection1_y)+1 + renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) + renderer.draw_rect(x, selection1_y, w, line_spacing, caret_color) + end + + local highlight_align = config.plugins.minimap.highlight_align + local highlight_width = config.plugins.minimap.highlight_width + local gutter_width = config.plugins.minimap.gutter_width + + -- time to draw the actual code, setup some local vars that are used in both highlighted and plain rendering. + local line_y = y + + -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. + local color = style.syntax["normal"] + color = {color[1], color[2], color[3], color[4] * 0.5} + + -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. + local batch_width = 0 + local batch_start = x + local last_batch_end = -1 + local minimap_cutoff_x = config.plugins.minimap.width * SCALE + local batch_syntax_type = nil + local function flush_batch(type, cache) + if batch_width > 0 then + local lastidx = #cache + local old_color = color + color = style.syntax[type] + if config.plugins.minimap.syntax_highlight and color ~= nil then + -- fetch and dim colors + color = {color[1], color[2], color[3], (color[4] or 255) * 0.5} + else + color = old_color + end + if #cache >= 3 then + local last_color = cache[lastidx] + if + last_batch_end == batch_start -- no space skipped + and ( + batch_syntax_type == type -- and same syntax + or ( -- or same color + last_color[1] == color[1] + and last_color[2] == color[2] + and last_color[3] == color[3] + and last_color[4] == color[4] + ) + ) + then + batch_start = cache[lastidx - 2] + batch_width = cache[lastidx - 1] + batch_width + lastidx = lastidx - 3 + end + end + cache[lastidx + 1] = batch_start + cache[lastidx + 2] = batch_width + cache[lastidx + 3] = color + end + batch_syntax_type = type + batch_start = batch_start + batch_width + last_batch_end = batch_start + batch_width = 0 + end + + local highlight_x + if highlight_align == 'left' then + highlight_x = x + else + highlight_x = x + w - highlight_width + end + local function render_highlight(idx, line_y) + local highlight_color = self:line_highlight_color(idx) + if highlight_color then + renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) + end + end + + local endidx = math.min(minimap_lines_start + minimap_lines_count, #self.dv.doc.lines) + + reset_cache_if_needed() + + if not highlighter_cache[dv.doc.highlighter] then + highlighter_cache[dv.doc.highlighter] = {} + end + + -- per line + for idx = minimap_lines_start, endidx do + batch_syntax_type = nil + batch_start = 0 + batch_width = 0 + last_batch_end = -1 + + render_highlight(idx, line_y) + local cache = highlighter_cache[dv.doc.highlighter][idx] + if not highlighter_cache[dv.doc.highlighter][idx] then -- need to cache + highlighter_cache[dv.doc.highlighter][idx] = {} + cache = highlighter_cache[dv.doc.highlighter][idx] + -- per token + for _, type, text in dv.doc.highlighter:each_token(idx) do + if not config.plugins.minimap.syntax_highlight then + type = nil + end + local start = 1 + while true do + -- find text followed spaces followed by newline + local s, e, w, eol = string.ufind(text, "[^%s]*()[ \t]*()\n?", start) + if not s then break end + local nchars = w - s + start = e + 1 + batch_width = batch_width + char_spacing * nchars + + local nspaces = 0 + for i=w,e do + local whitespace = string.sub(text, i, i) + if whitespace == "\t" then + nspaces = nspaces + config.plugins.minimap.tab_width + elseif whitespace == " " then + nspaces = nspaces + 1 + end + end + -- not enough spaces; consider them part of the batch + if nspaces < config.plugins.minimap.spaces_to_split then + batch_width = batch_width + nspaces * char_spacing + end + -- line has ended or no more space in the minimap; + -- we can go to the next line + if eol <= w or batch_start + batch_width > minimap_cutoff_x then + if batch_width > 0 then + flush_batch(type, cache) + end + break + end + -- enough spaces to split the batch + if nspaces >= config.plugins.minimap.spaces_to_split then + flush_batch(type, cache) + batch_start = batch_start + nspaces * char_spacing + end + end + end + end + -- draw from cache + for i=1,#cache,3 do + local batch_start = cache[i ] + x + gutter_width + local batch_width = cache[i + 1] + local color = cache[i + 2] + renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) + end + line_y = line_y + line_spacing + end end --- Overloaded so we can render the minimap in the "scrollbar area". -local prev_draw_scrollbar = DocView.draw_scrollbar -DocView.draw_scrollbar = function(self) - if not show_minimap() then return prev_draw_scrollbar(self) end - - local x, y, w, h = self:get_scrollbar_rect() - - local highlight = self.hovered_scrollbar or self.dragging_scrollbar - local visual_color = highlight and style.scrollbar2 or style.scrollbar - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - local scroller_height = visible_lines_count * line_spacing - local line_count = #self.doc.lines - - local visible_y = self.position.y + (visible_lines_start - 1) * line_spacing - - -- check if file is too large to fit inside the minimap area - local max_minmap_lines = math.floor(h / line_spacing) - local minimap_start_line = 1 - if is_file_too_large(self) then - - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled - - local scroll_pos_pixels = scroll_pos * (h - scroller_height) - visible_y = self.position.y + scroll_pos_pixels - - -- offset visible area if user is scrolling past end - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + scroller_height * (1.0 - t) end - - minimap_start_line = visible_lines_start - - math.floor(scroll_pos_pixels / line_spacing) - minimap_start_line = math.max(1, math.min(minimap_start_line, - line_count - max_minmap_lines)) - end - - if config.plugins.minimap.draw_background then - renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) - end - -- draw visual rect - renderer.draw_rect(x, visible_y, w, scroller_height, visual_color) - - -- highlight the selected lines, and the line with the caret on it - local selection_color = config.plugins.minimap.selection_color or style.dim - local caret_color = config.plugins.minimap.caret_color or style.caret - local selection_line, selection_col, selection_line2, selection_col2 = self.doc:get_selection() - local selection_y = y + (selection_line - minimap_start_line) * line_spacing - local selection2_y = y + (selection_line2 - minimap_start_line) * line_spacing - local selection_min_y = math.min(selection_y, selection2_y) - local selection_h = math.abs(selection2_y - selection_y)+1 - renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) - renderer.draw_rect(x, selection_y, w, line_spacing, caret_color) - - local highlight_align = config.plugins.minimap.highlight_align - local highlight_width = config.plugins.minimap.highlight_width - local gutter_width = config.plugins.minimap.gutter_width - - -- time to draw the actual code, setup some local vars that are used in both highlighted and plain renderind. - local line_y = y - - -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. - local color = style.syntax["normal"] - color = {color[1], color[2], color[3], color[4] * 0.5} - - -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. - local batch_width = 0 - local batch_start = x - local minimap_cutoff_x = x + config.plugins.minimap.width * SCALE - local batch_syntax_type = nil - local function flush_batch(type) - local old_color = color - color = style.syntax[batch_syntax_type] - if config.plugins.minimap.syntax_highlight and color ~= nil then - -- fetch and dim colors - color = {color[1], color[2], color[3], color[4] * 0.5} - else - color = old_color - end - if batch_width > 0 then - renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) - end - batch_syntax_type = type - batch_start = batch_start + batch_width - batch_width = 0 - end - - local highlight_x - if highlight_align == 'left' then - highlight_x = x - else - highlight_x = x + w - highlight_width - end - local function render_highlight(idx, line_y) - local highlight_color = minimap:line_highlight_color(idx) - if highlight_color then - renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) - end - end - - -- render lines with syntax highlighting - if config.plugins.minimap.syntax_highlight then - - -- keep track of the highlight type, since this needs to break batches as well - batch_syntax_type = nil - - -- per line - local endidx = minimap_start_line + max_minmap_lines - endidx = math.min(endidx, line_count) - for idx = minimap_start_line, endidx do - batch_syntax_type = nil - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - -- per token - for _, type, text in self.doc.highlighter:each_token(idx) do - -- flush prev batch - if not batch_syntax_type then batch_syntax_type = type end - if batch_syntax_type ~= type then flush_batch(type) end - - -- per character - for char in common.utf8_chars(text) do - if char == " " or char == "\n" then - flush_batch(type) - batch_start = batch_start + char_spacing - elseif char == " " then - flush_batch(type) - batch_start = batch_start + (char_spacing * config.plugins.minimap.tab_width) - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch(type) - break - else - batch_width = batch_width + char_spacing - end - - end - end - flush_batch(nil) - line_y = line_y + line_spacing - end - - else -- render lines without syntax highlighting - for idx = 1, line_count - 1 do - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - for char in common.utf8_chars(self.doc.lines[idx]) do - if char == " " or char == "\n" then - flush_batch() - batch_start = batch_start + char_spacing - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch() - else - batch_width = batch_width + char_spacing - end - end - flush_batch() - line_y = line_y + line_spacing - end - - end +local old_docview_new = DocView.new +function DocView:new(doc) + old_docview_new(self, doc) + if self:is(DocView) then self.v_scrollbar = MiniMap(self) end +end + +local old_docview_scroll_to_make_visible = DocView.scroll_to_make_visible +function DocView:scroll_to_make_visible(line, col, ...) + if + not self:is(DocView) or not self.v_scrollbar:is(MiniMap) + or + not self.v_scrollbar:is_minimap_enabled() + then + return old_docview_scroll_to_make_visible(self, line, col, ...) + end + local old_size = self.size.x + self.size.x = math.max(0, self.size.x - config.plugins.minimap.width) + local result = old_docview_scroll_to_make_visible(self, line, col, ...) + self.size.x = old_size + return result end -local prev_update = DocView.update -DocView.update = function (self) - if not show_minimap() then return prev_update(self) end - self.size.x = self.size.x - config.plugins.minimap.width * SCALE - return prev_update(self) + +local function get_all_docviews(node, t) + t = t or {} + if not node then return end + if node.type == "leaf" then + for i,v in ipairs(node.views) do + if v:is(DocView) then + table.insert(t, v) + end + end + end + get_all_docviews(node.a, t) + get_all_docviews(node.b, t) + return t end + command.add(nil, { - ["minimap:toggle-visibility"] = function() - config.plugins.minimap.enabled = not config.plugins.minimap.enabled - end, - ["minimap:toggle-syntax-highlighting"] = function() - config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight - end + ["minimap:toggle-visibility"] = function() + config.plugins.minimap.enabled = not config.plugins.minimap.enabled + for i,v in ipairs(get_all_docviews(core.root_view.root_node)) do + v.v_scrollbar.enabled = nil + end + end, + ["minimap:toggle-syntax-highlighting"] = function() + config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight + end +}) + +command.add("core.docview!", { + ["minimap:toggle-visibility-for-current-view"] = function() + local sb = core.active_view.v_scrollbar + if sb.enabled ~= nil then + sb.enabled = not sb.enabled + else + sb.enabled = not config.plugins.minimap.enabled + end + end }) -return minimap +return MiniMap |