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" -- General plugin settings config.minimap_enabled = true config.minimap_width = 100 config.minimap_instant_scroll = false config.minimap_syntax_highlight = true config.minimap_scale = 1 config.minimap_draw_background = true -- Configure size for rendering each char in the minimap local char_height = 1 * SCALE * config.minimap_scale local char_spacing = 0.8 * SCALE * config.minimap_scale local line_spacing = 2 * SCALE * config.minimap_scale -- 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 config.minimap_enabled 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 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 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 config.minimap_enabled 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.minimap_instant_scroll) return end end return prev_on_mouse_pressed(self, button, x, y, clicks) 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 config.minimap_enabled 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 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 config.minimap_enabled then return prev_get_scrollbar_rect(self) end return self.position.x + self.size.x - config.minimap_width * SCALE, self.position.y, config.minimap_width * SCALE, self.size.y 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 config.minimap_enabled 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.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) -- 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.minimap_width * SCALE -- render lines with syntax highlighting if config.minimap_syntax_highlight then -- keep track of the highlight type, since this needs to break batches as well local batch_syntax_type = nil local function flush_batch(type) if batch_width > 0 then -- fetch and dim colors color = style.syntax[batch_syntax_type] color = { color[1], color[2], color[3], color[4] * 0.5 } 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 -- 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 batch_width = 0 -- 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 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 local function flush_batch() if batch_width > 0 then renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) end batch_start = batch_start + batch_width batch_width = 0 end for idx=1,line_count-1 do batch_start = x batch_width = 0 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 end command.add(nil, { ["minimap:toggle-visibility"] = function() config.minimap_enabled = not config.minimap_enabled end, ["minimap:toggle-syntax-highlighting"] = function() config.minimap_syntax_highlight = not config.minimap_syntax_highlight end, })