diff --git a/README.md b/README.md index 0f52c6a..d44ca55 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,14 @@ vim.g.neominimap = { enabled = true, ---@type boolean }, + viewport = { + -- Highlight the visible source range on the minimap. + -- Only supported when layout == "float". + enabled = false, ---@type boolean + priority = 5, ---@type integer + hl_group = "NeominimapViewport", ---@type string + }, + --- Override the default window options ---@param opt vim.wo ---@param winid integer the window id of the source window, NOT the minimap window @@ -989,6 +997,7 @@ Checkout the wiki page for more details. [wiki](https://github.com/Isrothy/neomi | `NeominimapCursorLineNr` | To replace `CursorLineNr` in minimaps. | | `NeominimapCursorLineSign` | To replace `CursorLineSign` in minimaps. | | `NeominimapCursorLineFold` | To replace `CursorLineFold` in minimaps. | +| `NeominimapViewport` | Visible source range overlay on the minimap. (float only) | ### Highlight Groups of Diagnostic Annotations @@ -1086,11 +1095,10 @@ Checkout the wiki page for more details. [wiki](https://github.com/Isrothy/neomi Use [satellite.nvim](https://github.com/lewis6991/satellite.nvim), [nvim-scrollview](https://github.com/dstein64/nvim-scrollview) or other plugins. -- Display screen bounds like - [codewindow.nvim](https://github.com/gorbit99/codewindow.nvim). - For performance, this plugin creates a minimap buffer for each buffer. - Since a screen bound is a windowwise thing, - it's not impossible to display them by highlights. +- Display screen bounds in split layout. + Viewport overlay is supported in float layout. + For split layout, the minimap is shared across all windows in a tab, + making window-wise viewport display infeasible. ## Limitations diff --git a/doc/neominimap.nvim.txt b/doc/neominimap.nvim.txt index a4a1163..e72edfd 100644 --- a/doc/neominimap.nvim.txt +++ b/doc/neominimap.nvim.txt @@ -382,7 +382,15 @@ Default configuration ~ -- Consider folds when rendering the minimap enabled = true, ---@type boolean }, - + + viewport = { + -- Highlight the visible source range on the minimap. + -- Only supported when layout == "float". + enabled = false, ---@type boolean + priority = 5, ---@type integer + hl_group = "NeominimapViewport", ---@type string + }, + --- Override the default window options ---@param opt vim.wo ---@param winid integer the window id of the source window, NOT the minimap window @@ -927,6 +935,9 @@ HIGHLIGHT GROUPS OF NEOMINIMAP WINDOWS ~ NeominimapCursorLineSign To replace CursorLineSign in minimaps. NeominimapCursorLineFold To replace CursorLineFold in minimaps. + + NeominimapViewport Visible source range overlay on the + minimap. (float only) ----------------------------------------------------------------------- HIGHLIGHT GROUPS OF DIAGNOSTIC ANNOTATIONS ~ @@ -1021,11 +1032,10 @@ NON-GOALS *neominimap.nvim-neominimap-non-goals* Use satellite.nvim , nvim-scrollview or other plugins. -- Display screen bounds like - codewindow.nvim . - For performance, this plugin creates a minimap buffer for each buffer. - Since a screen bound is a windowwise thing, - it’s not impossible to display them by highlights. +- Display screen bounds in split layout. + Viewport overlay is supported in float layout. + For split layout, the minimap is shared across all windows in a tab, + making window-wise viewport display infeasible. LIMITATIONS *neominimap.nvim-neominimap-limitations* diff --git a/lua/neominimap/config/internal.lua b/lua/neominimap/config/internal.lua index bfe7e63..a33d2e2 100644 --- a/lua/neominimap/config/internal.lua +++ b/lua/neominimap/config/internal.lua @@ -205,6 +205,12 @@ local M = { enabled = true, ---@type boolean }, + viewport = { + enabled = false, ---@type boolean + priority = 5, ---@type integer + hl_group = "NeominimapViewport", ---@type string + }, + --- Override the default window options ---@param opt vim.wo ---@param winid integer the window id of the source window, NOT the minimap window diff --git a/lua/neominimap/config/meta.lua b/lua/neominimap/config/meta.lua index e3b28d3..600fa60 100644 --- a/lua/neominimap/config/meta.lua +++ b/lua/neominimap/config/meta.lua @@ -27,6 +27,7 @@ local M = {} ---@field search? Neominimap.SearchConfig ---@field mark? Neominimap.MarkConfig ---@field fold? Neominimap.FoldConfig +---@field viewport? Neominimap.ViewportConfig ---@field winopt? fun(opt: vim.wo, winid: integer) ---@field bufopt? fun(opt: vim.bo, bufnr: integer) ---@field handler? Neominimap.Map.Handler[] @@ -92,6 +93,11 @@ local M = {} ---@class (exact) Neominimap.FoldConfig ---@field enabled? boolean +---@class (exact) Neominimap.ViewportConfig +---@field enabled? boolean +---@field priority? integer +---@field hl_group? string + ---@type Neominimap.UserConfig | fun():Neominimap.UserConfig | nil vim.g.neominimap = vim.g.neominimap diff --git a/lua/neominimap/config/validator.lua b/lua/neominimap/config/validator.lua index 350f85c..df8dd19 100644 --- a/lua/neominimap/config/validator.lua +++ b/lua/neominimap/config/validator.lua @@ -162,6 +162,11 @@ M.validate_config = function(cfg) fold = { cfg.fold, "table" }, ["fold.enabled"] = { cfg.fold.enabled, "boolean" }, + viewport = { cfg.viewport, "table" }, + ["viewport.enabled"] = { cfg.viewport.enabled, "boolean" }, + ["viewport.priority"] = { cfg.viewport.priority, "number" }, + ["viewport.hl_group"] = { cfg.viewport.hl_group, "string" }, + winopt = { cfg.winopt, { "table", "function" } }, bufopt = { cfg.bufopt, { "table", "function" } }, diff --git a/lua/neominimap/window/float/autocmds.lua b/lua/neominimap/window/float/autocmds.lua index bd9b523..d34b692 100644 --- a/lua/neominimap/window/float/autocmds.lua +++ b/lua/neominimap/window/float/autocmds.lua @@ -46,12 +46,16 @@ M.on_win_closed = function(args) return end local window_map = require("neominimap.window.float.window_map") - if window_map.is_minimap_window(winid) and not config.float.persist then - logger.log.trace("This is a minimap window. Close minimap for current window") - local var = require("neominimap.variables") - local swinid = window_map.get_parent_winid(winid) - if swinid then - var.w[swinid].enabled = false + if window_map.is_minimap_window(winid) then + logger.log.trace("This is a minimap window. Clearing viewport for %d", winid) + require("neominimap.window.viewport").clear(winid) + if not config.float.persist then + logger.log.trace("Close minimap for current window") + local var = require("neominimap.variables") + local swinid = window_map.get_parent_winid(winid) + if swinid then + var.w[swinid].enabled = false + end end end vim.schedule(function() @@ -149,9 +153,9 @@ M.on_minimap_buffer_text_changed = function(args) local win_list = require("neominimap.util").get_attached_window(bufnr) vim.schedule(function() for _, winid in ipairs(win_list) do - logger.log.trace("Resetting cursor line for window %d.", winid) - require("neominimap.window.float.internal").reset_mwindow_cursor_line(winid) - logger.log.trace("Cursor line reset for window %d.", winid) + logger.log.trace("Handling minimap buffer text change for window %d.", winid) + require("neominimap.window.float.internal").on_minimap_buffer_text_changed(winid) + logger.log.trace("Minimap buffer text change handled for window %d.", winid) end end) end diff --git a/lua/neominimap/window/float/internal.lua b/lua/neominimap/window/float/internal.lua index 7d2936f..4987aa8 100644 --- a/lua/neominimap/window/float/internal.lua +++ b/lua/neominimap/window/float/internal.lua @@ -184,6 +184,7 @@ M.close_minimap_window = function(winid) logger.log.trace("Attempting to close minimap for window %d", winid) if mwinid and api.nvim_win_is_valid(mwinid) then logger.log.trace("Deleting minimap window %d", mwinid) + require("neominimap.window.viewport").clear(mwinid) local util = require("neominimap.util") util.noautocmd(api.nvim_win_close)(mwinid, true) return mwinid @@ -242,6 +243,8 @@ M.refresh_minimap_window = function(winid) M.reset_mwindow_cursor_line(winid) + require("neominimap.window.viewport").refresh(winid, mwinid) + logger.log.trace("Minimap for window %d refreshed", winid) return mwinid end @@ -296,6 +299,23 @@ M.reset_mwindow_cursor_line = function(winid) return true end +--- Called when minimap buffer text is updated. +--- Refreshes cursor line and viewport overlay without reconfiguring the window. +---@param winid integer +M.on_minimap_buffer_text_changed = function(winid) + local logger = require("neominimap.logger") + local window_map = require("neominimap.window.float.window_map") + logger.log.trace("Handling minimap buffer text change for window %d", winid) + local mwinid = window_map.get_minimap_winid(winid) + if not mwinid or not api.nvim_win_is_valid(mwinid) then + logger.log.trace("Minimap window is not valid for %d", winid) + return + end + require("neominimap.window.util").sync_to_source(winid, mwinid) + require("neominimap.window.viewport").refresh(winid, mwinid) + logger.log.trace("Minimap buffer text change handled for window %d", winid) +end + ---@param mwinid integer ---@return boolean M.reset_parent_window_cursor_line = function(mwinid) diff --git a/lua/neominimap/window/init.lua b/lua/neominimap/window/init.lua index 3d1c2a6..8b5e444 100644 --- a/lua/neominimap/window/init.lua +++ b/lua/neominimap/window/init.lua @@ -8,6 +8,7 @@ api.nvim_set_hl(0, "NeominimapCursorLine", { link = "CursorLine", default = true api.nvim_set_hl(0, "NeominimapCursorLineSign", { link = "CursorLineSign", default = true }) api.nvim_set_hl(0, "NeominimapCursorLineNr", { link = "CursorLineSign", default = true }) api.nvim_set_hl(0, "NeominimapCursorLineFold", { link = "CursorLineSign", default = true }) +api.nvim_set_hl(0, "NeominimapViewport", { link = "Visual", default = true }) ---@class Neominimap.Window ---@field create_autocmds fun(group: string | integer) diff --git a/lua/neominimap/window/viewport.lua b/lua/neominimap/window/viewport.lua new file mode 100644 index 0000000..26af764 --- /dev/null +++ b/lua/neominimap/window/viewport.lua @@ -0,0 +1,102 @@ +local M = {} +local api = vim.api +local config = require("neominimap.config") +local coord = require("neominimap.map.coord") +local fold = require("neominimap.map.fold") +local logger = require("neominimap.logger") + +---@class Neominimap.Viewport.Cache +---@field w0 integer +---@field w_dollar integer +---@field sbufnr integer +---@field mbufnr integer +---@field line_count integer + +---@type table +local cache = {} + +---@type table +local match_id_map = {} + +---@param swinid integer +---@param mwinid integer +M.refresh = function(swinid, mwinid) + if not config.viewport.enabled then + return + end + if not api.nvim_win_is_valid(swinid) or not api.nvim_win_is_valid(mwinid) then + return + end + + local mbufnr = api.nvim_win_get_buf(mwinid) + if not mbufnr or not api.nvim_buf_is_valid(mbufnr) then + return + end + + local sbufnr = api.nvim_win_get_buf(swinid) + if not sbufnr or not api.nvim_buf_is_valid(sbufnr) then + return + end + + local w0 = vim.fn.line("w0", swinid) + local w_dollar = vim.fn.line("w$", swinid) + if w0 == 0 or w_dollar == 0 then + return + end + + local cached_folds = fold.get_cached_folds(sbufnr) or {} + local start_row, end_row = fold.get_visible_range(cached_folds, w0, w_dollar) + local m_start_row = coord.codepoint_to_mcodepoint(start_row, 1) + local m_end_row = coord.codepoint_to_mcodepoint(end_row, 1) + local line_count = api.nvim_buf_line_count(mbufnr) + + local cached = cache[mwinid] + if + cached + and cached.w0 == w0 + and cached.w_dollar == w_dollar + and cached.sbufnr == sbufnr + and cached.mbufnr == mbufnr + and cached.line_count == line_count + then + return + end + + logger.log.trace("Refreshing viewport overlay for minimap window %d", mwinid) + + local old_match_id = match_id_map[mwinid] + if old_match_id then + pcall(vim.fn.matchdelete, old_match_id, mwinid) + end + + if m_start_row > line_count then + cache[mwinid] = { w0 = w0, w_dollar = w_dollar, sbufnr = sbufnr, mbufnr = mbufnr, line_count = line_count } + logger.log.trace("Viewport overlay cleared (out of range) for minimap window %d", mwinid) + return + end + + local end_line = math.min(m_end_row, line_count) + local pattern = string.format([[\m\%%>%dl\%%<%dl.*]], m_start_row - 1, end_line + 1) + local ok, match_id = + pcall(vim.fn.matchadd, config.viewport.hl_group, pattern, config.viewport.priority, -1, { window = mwinid }) + if ok and match_id ~= -1 then + match_id_map[mwinid] = match_id + else + logger.log.warn("Failed to add viewport match for window %d: %s", mwinid, tostring(match_id)) + end + + cache[mwinid] = { w0 = w0, w_dollar = w_dollar, sbufnr = sbufnr, mbufnr = mbufnr, line_count = line_count } + logger.log.trace("Viewport overlay refreshed for minimap window %d", mwinid) +end + +---@param mwinid integer +M.clear = function(mwinid) + local match_id = match_id_map[mwinid] + if match_id then + pcall(vim.fn.matchdelete, match_id, mwinid) + end + match_id_map[mwinid] = nil + cache[mwinid] = nil +end + +return M