feat: Better highlight-hover/follow-cursor procedures

Previously on each outline open, the `writer.make_outline` function
might be called at least 4 times(!), after this refactor it will only be
called once. And on update cursor autocmds, also called once (previously
at least twice).

behaviour:
- Now the outline window focus and highlight can update on each cursor
  move (previously CursorHold, dependent on updatetime). This is now
  configurable as well.

- During fold-all/unfold-all operations, now the cursor will remain on
  the same node (rather than same line in outline buffer).

- The performance improvement is not significantly observable since even
  the old implementation can appear instant. One may even argue I am
  fixing a problem that did not exist, but implementation-wise it's just
  so much better now.

config:
- outline_window.auto_update_events, list of events to be passed to
  create_user_autocmd for updating cursor focus in outline, and updating
  outline items (refetching symbols), using keys cursor and items
  respectively.

- outline_window.show_cursorline now supports 2 other string values:
  'focus_in_outline'/'focus_in_code' which controls when to enable
  cursorline. Setting to true retains the default behaviour of always
  showing the cursorline. This was added because now that the cursor
  focus on the outline could change on each CursorMoved, the cursorline
  may pose to be qute attention-seeking during the outline cursor
  updates. Hence `focus_in_outline` is added so that when focus is in
  code, the cursorline for outline window is not shown.

  'focus_in_code' is added so that a user who disabled
  highlight_hovered_item can keep track of position in outline when
  focus is in code, disabling cursorline when focus is in outline.

  At any given time, if hide cursor is enabled and show_cursorline is a
  string value, hiding of cursor will not be done if cursorline is not
  shown in the the given situation.

implementation:
- The reason for the improvement in performance as described in the
  first paragraph is due to merging of finding hover item and finding
  the deepest matched node to put cursor, into writer.make_outline. This
  done, when previously done in separate function, because after the
  separate function (namely _highlight_hovered_item) finishes,
  writer.make_outline is called *again* anyway.

- Autocmds to update cursor position in outline is now done per buffer
  rather than global.

Somehow the auto unfold and unfold depth options still work perfectly,
for this we should thank simrat or which ever contributor that
modularized the folding module and made it adaptable :)
This commit is contained in:
hedy
2023-11-18 09:34:16 +08:00
parent a1aa652fb2
commit d35ee70f95
6 changed files with 149 additions and 94 deletions

View File

@@ -31,11 +31,16 @@ M.defaults = {
auto_jump = false,
show_numbers = false,
show_relative_numbers = false,
---@type boolean?|string?
show_cursorline = true,
hide_cursor = false,
winhl = 'OutlineDetails:Comment,OutlineLineno:LineNr',
jump_highlight_duration = 500,
jump_highlight_duration = 400,
center_on_jump = true,
auto_update_events = {
cursor = { 'CursorMoved' },
items = { 'InsertLeave', 'WinEnter', 'BufEnter', 'BufWinEnter', 'TabEnter', 'BufWritePost' },
},
},
preview_window = {
auto_preview = false,

View File

@@ -10,27 +10,12 @@ local writer = require('outline.writer')
local M = {}
local function setup_global_autocmd()
if cfg.o.outline_items.highlight_hovered_item or cfg.o.symbol_folding.auto_unfold_hover then
vim.api.nvim_create_autocmd('CursorHold', {
pattern = '*',
callback = function()
M._highlight_current_item(nil)
end,
})
end
vim.api.nvim_create_autocmd({
'InsertLeave',
'WinEnter',
'BufEnter',
'BufWinEnter',
'TabEnter',
'BufWritePost',
}, {
if utils.table_has_content(cfg.o.outline_window.auto_update_events.items) then
vim.api.nvim_create_autocmd(cfg.o.outline_window.auto_update_events.items, {
pattern = '*',
callback = M._refresh,
})
end
vim.api.nvim_create_autocmd('WinEnter', {
pattern = '*',
callback = require('outline.preview').close,
@@ -41,11 +26,13 @@ end
-- STATE
-------------------------
M.state = {
opened_first_outline = false,
---@type outline.SymbolNode[]
outline_items = {},
---@type outline.FlatSymbolNode[]
flattened_outline_items = {},
code_win = 0,
autocmds = {},
-- In case unhide_cursor was called before hide_cursor for _some_ reason,
-- this can still be used as a fallback
original_cursor = vim.o.guicursor,
@@ -56,13 +43,18 @@ local function wipe_state()
outline_items = {},
flattened_outline_items = {},
code_win = 0,
autocmds = {},
opts = {},
}
end
local function _update_lines()
M.state.flattened_outline_items =
writer.make_outline(M.view.bufnr, M.state.outline_items, M.state.code_win)
local function _update_lines(update_cursor, set_cursor_to_node)
local current
M.state.flattened_outline_items, current =
writer.make_outline(M.view.bufnr, M.state.outline_items, M.state.code_win, set_cursor_to_node)
if update_cursor ~= false then
M.update_cursor_pos(current)
end
end
---@param items outline.SymbolNode[]
@@ -78,7 +70,29 @@ local function __refresh()
return
end
M.state.code_win = vim.api.nvim_get_current_win()
local curwin = vim.api.nvim_get_current_win()
if M.state.codewin ~= curwin then
if M.state.autocmds[M.state.codewin] then
vim.api.nvim_del_autocmd(M.state.autocmds[M.state.codewin])
end
M.state.codewin = curwin
end
if cfg.o.outline_items.highlight_hovered_item or cfg.o.symbol_folding.auto_unfold_hover then
if M.state.autocmds[M.state.code_win] then
vim.api.nvim_del_autocmd(M.state.autocmds[M.state.code_win])
end
if utils.str_or_nonempty_table(cfg.o.outline_window.auto_update_events.cursor) then
M.state.autocmds[M.state.code_win] = vim.api.nvim_create_autocmd(
cfg.o.outline_window.auto_update_events.cursor,
{
buffer = vim.api.nvim_win_get_buf(M.state.code_win),
callback = function() M._highlight_current_item(nil) end
}
)
end
end
local items = parser.parse(response, vim.api.nvim_get_current_buf())
_merge_items(items)
@@ -156,19 +170,37 @@ function M._toggle_fold(move_cursor, node_index)
end
end
local function hide_cursor()
local function update_cursor_style()
local cl = cfg.o.outline_window.show_cursorline
-- XXX: Still 'hide' cursor if show_cursorline set to false, because we've
-- already warned the user during setup.
local hide_cursor = type(cl) ~= 'string'
if cl == 'focus_in_outline' or cl == 'focus_in_code' then
vim.api.nvim_win_set_option(0, "cursorline", cl == 'focus_in_outline')
hide_cursor = cl == 'focus_in_outline'
end
-- Set cursor color to CursorLine in normal mode
if hide_cursor then
M.state.original_cursor = vim.o.guicursor
local cur = vim.o.guicursor:match('n.-:(.-)[-,]')
vim.opt.guicursor:append('n:' .. cur .. '-Cursorline')
end
end
local function unhide_cursor()
local function reset_cursor_style()
local cl = cfg.o.outline_window.show_cursorline
if cl == 'focus_in_outline' or cl == 'focus_in_code' then
vim.api.nvim_win_set_option(0, "cursorline", cl ~= 'focus_in_outline')
end
-- vim.opt doesn't seem to provide a way to remove last item, like a pop()
-- vim.o.guicursor = vim.o.guicursor:gsub(",n.-:.-$", "")
vim.o.guicursor = M.state.original_cursor
end
---Autocmds for the (current) outline buffer
local function setup_buffer_autocmd()
if cfg.o.preview_window.auto_preview then
vim.api.nvim_create_autocmd('CursorMoved', {
@@ -190,17 +222,17 @@ local function setup_buffer_autocmd()
end,
})
end
if cfg.o.outline_window.hide_cursor then
if cfg.o.outline_window.hide_cursor or type(cfg.o.outline_window.show_cursorline) == 'string' then
-- Unfortunately guicursor is a global option, so we have to make sure to
-- set and unset when cursor leaves the outline window.
hide_cursor()
update_cursor_style()
vim.api.nvim_create_autocmd('BufEnter', {
buffer = 0,
callback = hide_cursor,
callback = update_cursor_style,
})
vim.api.nvim_create_autocmd('BufLeave', {
buffer = 0,
callback = unhide_cursor,
callback = reset_cursor_style,
})
end
end
@@ -219,7 +251,7 @@ function M._set_folded(folded, move_cursor, node_index)
vim.api.nvim_win_set_cursor(M.view.winnr, { node_index, 0 })
end
_update_lines()
_update_lines(false)
elseif node.parent then
local parent_node = M.state.flattened_outline_items[node.parent.line_in_outline]
@@ -248,6 +280,7 @@ end
---@param nodes? outline.SymbolNode[]
function M._set_all_folded(folded, nodes)
local stack = { nodes or M.state.outline_items }
local current = M._current_node()
while #stack > 0 do
local current_nodes = table.remove(stack, #stack)
@@ -259,7 +292,7 @@ function M._set_all_folded(folded, nodes)
end
end
_update_lines()
_update_lines(true, current)
end
---@param winnr? integer Window number of code window
@@ -290,59 +323,9 @@ function M._highlight_current_item(winnr)
-- parents folded)
-- In one go
local win = winnr or vim.api.nvim_get_current_win()
local buf = vim.api.nvim_win_get_buf(win)
-- XXX: Could current win ~= M.state.codewin here?
local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1
local parent_nodes = {}
-- Must not skip folded nodes so that when user unfolds a parent, they can see the leaf
-- node highlighted.
for value in
parser.preorder_iter(M.state.outline_items, function()
return true
end)
do
value.hovered = nil
if
value.line == hovered_line
or (hovered_line >= value.range_start and hovered_line <= value.range_end)
then
value.hovered = true
table.insert(parent_nodes, value)
end
end
if #parent_nodes == 0 then
return
end
-- Probably can't 'just' writer.add_hover_highlights here because we might
-- want to auto_unfold_hover
_update_lines()
-- Put cursor on deepest visible match
local col = 0
if cfg.o.outline_items.show_symbol_lineno then
-- Padding area between lineno column and start of guides
col = #tostring(vim.api.nvim_buf_line_count(buf) - 1)
end
local flats = M.state.flattened_outline_items
local found = false
local find_node
while #parent_nodes > 0 and not found do
find_node = table.remove(parent_nodes, #parent_nodes)
-- TODO: Is it feasible to use binary search here?
for line, node in ipairs(flats) do
if node == find_node then
vim.api.nvim_win_set_cursor(M.view.winnr, { line, col })
found = true
break
end
end
end
_update_lines(true)
end
local function setup_keymaps(bufnr)
@@ -409,6 +392,19 @@ local function setup_keymaps(bufnr)
end)
end
---@param current outline.FlatSymbolNode?
function M.update_cursor_pos(current)
local col = 0
local buf = vim.api.nvim_win_get_buf(M.state.code_win)
if cfg.o.outline_items.show_symbol_lineno then
-- Padding area between lineno column and start of guides
col = #tostring(vim.api.nvim_buf_line_count(buf) - 1)
end
if current then -- Don't attempt to set cursor if the matching node is not found
vim.api.nvim_win_set_cursor(M.view.winnr, { current.line_in_outline, col })
end
end
---@param response table?
---@param opts outline.OutlineOpts?
local function handler(response, opts)
@@ -417,6 +413,7 @@ local function handler(response, opts)
end
M.state.code_win = vim.api.nvim_get_current_win()
M.state.opened_first_outline = true
if opts and opts.on_symbols then
opts.on_symbols()
@@ -428,6 +425,21 @@ local function handler(response, opts)
opts.on_outline_setup()
end
if cfg.o.outline_items.highlight_hovered_item or cfg.o.symbol_folding.auto_unfold_hover then
if M.state.autocmds[M.state.code_win] then
vim.api.nvim_del_autocmd(M.state.autocmds[M.state.code_win])
end
if utils.str_or_nonempty_table(cfg.o.outline_window.auto_update_events.cursor) then
M.state.autocmds[M.state.code_win] = vim.api.nvim_create_autocmd(
cfg.o.outline_window.auto_update_events.cursor,
{
buffer = vim.api.nvim_win_get_buf(M.state.code_win),
callback = function() M._highlight_current_item(nil) end
}
)
end
end
-- clear state when buffer is closed
vim.api.nvim_buf_attach(M.view.bufnr, false, {
on_detach = function(_, _)
@@ -441,9 +453,10 @@ local function handler(response, opts)
local items = parser.parse(response, vim.api.nvim_win_get_buf(M.state.code_win))
M.state.outline_items = items
M.state.flattened_outline_items = writer.make_outline(M.view.bufnr, items, M.state.code_win)
local current
M.state.flattened_outline_items, current = writer.make_outline(M.view.bufnr, items, M.state.code_win)
M._highlight_current_item(M.state.code_win)
M.update_cursor_pos(current)
if not cfg.o.outline_window.focus_on_open or (opts and not opts.focus_outline) then
vim.fn.win_gotoid(M.state.code_win)
@@ -667,6 +680,10 @@ With bang, don't switch cursor focus to outline window.",
nargs = 0,
bang = true,
})
cmd('Refresh', __refresh, {
desc = "Trigger manual outline refresh of items.",
nargs = 0,
})
end
---Set up configuration options for outline.

View File

@@ -37,6 +37,7 @@ local function get_width_offset()
width = width + 4
end
-- FIXME: use actual window position based on view rather than config
if cfg.o.outline_window.position == 'right' then
width = 0 - width
else

View File

@@ -111,7 +111,7 @@ function M.flash_highlight(winnr, lnum, durationMs, hl_group)
end
hl_group = hl_group or 'Visual'
if durationMs == true or durationMs == 1 then
durationMs = 500
durationMs = 400
end
local bufnr = vim.api.nvim_win_get_buf(winnr)
local ns = vim.api.nvim_buf_add_highlight(bufnr, 0, hl_group, lnum - 1, 0, -1)
@@ -137,4 +137,14 @@ function M.echo(module, message)
vim.api.nvim_echo({ prefix_chunk, { message } }, true, {})
end
---@param t table
function M.table_has_content(t)
return t and next(t) ~= nil
end
---@param t table|string
function M.str_or_nonempty_table(t)
return type(t) == 'string' or M.table_has_content(t)
end
return M

View File

@@ -52,7 +52,8 @@ function View:setup_view()
vim.api.nvim_win_set_option(self.winnr, 'rnu', true)
end
if cfg.o.outline_window.show_cursorline then
local cl = cfg.o.outline_window.show_cursorline
if cl == true or cl == 'focus_in_outline' then
vim.api.nvim_win_set_option(self.winnr, 'cursorline', true)
end
end

View File

@@ -66,13 +66,18 @@ end
-- Handles highlights, virtual text, and of course lines of outline to write
---@param bufnr integer Nothing is done if is_buffer_outline(bufnr) is not true
---@param items outline.SymbolNode[] Tree of symbols after being parsed by parser.parse_result
---@return outline.FlatSymbolNode[] flattened_items Empty table returned if bufnr is invalid
---@param codewin integer code window
function M.make_outline(bufnr, items, codewin)
---@param find_node outline.FlatSymbolNode|outline.SymbolNode? Find a given node rather than node matching cursor position in codewin
---@return outline.FlatSymbolNode[],outline.FlatSymbolNode? flattened_items Empty table returned if bufnr is invalid
function M.make_outline(bufnr, items, codewin, find_node)
if not M.is_buffer_outline(bufnr) then
return {}
return {}, nil
end
local codebuf = vim.api.nvim_win_get_buf(codewin)
-- 0-indexed
local hovered_line = vim.api.nvim_win_get_cursor(codewin)[1] - 1
-- Deepest matching node to put cursor on based on hovered line
local put_cursor
clear_virt_text(bufnr)
@@ -131,6 +136,22 @@ function M.make_outline(bufnr, items, codewin)
local fold_markers = cfg.o.symbol_folding.markers
for node in parser.preorder_iter(items) do
node.hovered = false
if
node.line == hovered_line
or (hovered_line >= node.range_start and hovered_line <= node.range_end)
then
-- XXX: not setting for children, but it works because when unfold is called
-- this function is called again anyway.
node.hovered = true
if not find_node then
put_cursor = node
end
end
if find_node and find_node == node then
put_cursor = find_node
end
table.insert(flattened, node)
node.line_in_outline = #flattened
table.insert(details, node.detail or '')
@@ -249,7 +270,7 @@ function M.make_outline(bufnr, items, codewin)
end
end
return flattened
return flattened, put_cursor
end
-- XXX: Is the performance tradeoff of calling `nvim_buf_set_lines` on each
-- iteration worth it in order to put setting of highlights, details, and