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 :)
290 lines
9.8 KiB
Lua
290 lines
9.8 KiB
Lua
local cfg = require('outline.config')
|
|
local folding = require('outline.folding')
|
|
local parser = require('outline.parser')
|
|
local symbols = require('outline.symbols')
|
|
local t_utils = require('outline.utils.table')
|
|
local ui = require('outline.ui')
|
|
|
|
local strlen = vim.fn.strlen
|
|
|
|
local M = {}
|
|
|
|
local hlns = vim.api.nvim_create_namespace('outline-icon-highlight')
|
|
local ns = vim.api.nvim_create_namespace('outline-virt-text')
|
|
|
|
---@param bufnr integer
|
|
---@return boolean
|
|
function M.is_buffer_outline(bufnr)
|
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
|
return false
|
|
end
|
|
local name = vim.api.nvim_buf_get_name(bufnr)
|
|
local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype')
|
|
return string.match(name, 'OUTLINE') ~= nil and ft == 'Outline'
|
|
end
|
|
|
|
---Apply highlights and hover highlights to bufnr
|
|
---@param bufnr integer
|
|
---@param nodes outline.FlatSymbolNode[] flattened nodes
|
|
function M.add_highlights(bufnr, hl_info, nodes)
|
|
for _, line_hl in ipairs(hl_info) do
|
|
local line, hl_start, hl_end, hl_type = unpack(line_hl)
|
|
vim.api.nvim_buf_add_highlight(bufnr, hlns, hl_type, line - 1, hl_start, hl_end)
|
|
end
|
|
M.add_hover_highlights(bufnr, nodes)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
local function clear_virt_text(bufnr)
|
|
vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param nodes outline.FlatSymbolNode[] flattened nodes
|
|
function M.add_hover_highlights(bufnr, nodes)
|
|
if not cfg.o.outline_items.highlight_hovered_item then
|
|
return
|
|
end
|
|
|
|
-- clear old highlight
|
|
ui.clear_hover_highlight(bufnr)
|
|
for _, node in ipairs(nodes) do
|
|
if not node.hovered then
|
|
goto continue
|
|
end
|
|
|
|
if node.prefix_length then
|
|
ui.add_hover_highlight(bufnr, node.line_in_outline - 1, node.prefix_length)
|
|
end
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
---The quintessential function of this entire plugin. Clears virtual text,
|
|
-- parses each node and replaces old lines with new lines to be written for the
|
|
-- outline buffer.
|
|
-- 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
|
|
---@param codewin integer code window
|
|
---@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 {}, 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)
|
|
|
|
---@type string[]
|
|
local lines = {}
|
|
---@type string[]
|
|
local details = {}
|
|
---@type string[]
|
|
local linenos = {}
|
|
---@type outline.FlatSymbolNode[]
|
|
local flattened = {}
|
|
local hl = {}
|
|
|
|
-- Find the prefix for each line needed for the lineno space
|
|
local lineno_offset = 0
|
|
local lineno_prefix = ''
|
|
local lineno_max_width = #tostring(vim.api.nvim_buf_line_count(codebuf) - 1)
|
|
if cfg.o.outline_items.show_symbol_lineno then
|
|
-- Use max width-1 plus 1 space padding.
|
|
-- -1 because if max_width is a power of ten, don't shift the entire lineno
|
|
-- column by the right just because the last line number requires an extra
|
|
-- digit. If max_width is 1000, the lineno column will take up 3 columns to
|
|
-- fill the digits, and 1 padding on the right. The 1000 can fit perfectly
|
|
-- there.
|
|
lineno_offset = math.max(2, lineno_max_width) + 1
|
|
lineno_prefix = string.rep(' ', lineno_offset)
|
|
end
|
|
|
|
-- Closures for convenience
|
|
local function add_guide_hl(from, to)
|
|
table.insert(hl, {
|
|
#flattened,
|
|
from,
|
|
to,
|
|
'OutlineGuides',
|
|
})
|
|
end
|
|
|
|
local function add_fold_hl(from, to)
|
|
table.insert(hl, {
|
|
#flattened,
|
|
from,
|
|
to,
|
|
'OutlineFoldMarker',
|
|
})
|
|
end
|
|
|
|
local guide_markers = cfg.o.guides.markers
|
|
if not cfg.o.guides.enabled then
|
|
guide_markers = {
|
|
middle = ' ',
|
|
vertical = ' ',
|
|
bottom = ' ',
|
|
}
|
|
end
|
|
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 '')
|
|
local lineno = tostring(node.range_start + 1)
|
|
local leftpad = string.rep(' ', lineno_max_width - #lineno)
|
|
table.insert(linenos, leftpad .. lineno)
|
|
|
|
-- Make the guides for the line prefix
|
|
local pref = t_utils.str_to_table(string.rep(' ', node.depth))
|
|
local fold_marker_width = 0
|
|
|
|
if folding.is_foldable(node) then
|
|
-- Add fold marker
|
|
local marker = fold_markers[2]
|
|
if folding.is_folded(node) then
|
|
marker = fold_markers[1]
|
|
end
|
|
pref[#pref] = marker
|
|
fold_marker_width = strlen(marker)
|
|
else
|
|
-- Rightmost guide for the immediate parent, only added if fold marker is
|
|
-- not added
|
|
if node.depth > 1 then
|
|
local marker = guide_markers.middle
|
|
if node.isLast then
|
|
marker = guide_markers.bottom
|
|
end
|
|
pref[#pref] = marker
|
|
end
|
|
end
|
|
|
|
-- Add vertical guides to the left, for all parents that isn't the last
|
|
-- sibling. Iter from first grandparent until second last ancestor (last
|
|
-- ancestor is the entire outline itself, it should not have a vertical
|
|
-- guide).
|
|
local iternode = node
|
|
for i = node.depth - 1, 2, -1 do
|
|
iternode = iternode.parent_node
|
|
if not iternode.isLast then
|
|
pref[i] = guide_markers.vertical
|
|
end
|
|
end
|
|
|
|
-- Finished with guide prefix
|
|
-- Join all prefix chars by a space
|
|
local pref_str = table.concat(pref, ' ')
|
|
local total_pref_len = lineno_offset + #pref_str
|
|
|
|
-- Guide hl goes from start of prefix till before the fold marker, if any.
|
|
-- Fold hl goes from start of fold marker until before the icon.
|
|
add_guide_hl(lineno_offset, total_pref_len - fold_marker_width)
|
|
if fold_marker_width > 0 then
|
|
add_fold_hl(total_pref_len - fold_marker_width, total_pref_len + 1)
|
|
end
|
|
|
|
local line = lineno_prefix .. pref_str
|
|
local icon_pref = 0
|
|
if node.icon ~= '' then
|
|
line = line .. ' ' .. node.icon
|
|
icon_pref = 1
|
|
end
|
|
line = line .. ' ' .. node.name
|
|
|
|
-- Highlight for the icon ✨
|
|
-- Start from icon col
|
|
local hl_start = #pref_str + #lineno_prefix + icon_pref
|
|
local hl_end = hl_start + #node.icon -- until after icon
|
|
local hl_type = cfg.o.symbols.icons[symbols.kinds[node.kind]].hl
|
|
table.insert(hl, { #flattened, hl_start, hl_end, hl_type })
|
|
|
|
-- Prefix length is from start until the beginning of the node.name, used
|
|
-- for hover highlights.
|
|
node.prefix_length = hl_end + 1
|
|
|
|
-- lines passed to nvim_buf_set_lines cannot contain newlines in each line
|
|
line = line:gsub('\n', ' ')
|
|
table.insert(lines, line)
|
|
end
|
|
|
|
-- Write the lines 🎉
|
|
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
|
|
|
-- Unfortunately highlights and extmarks cannot be added to lines that do not
|
|
-- yet exist. Hence this requires another O(n) of iteration.
|
|
M.add_highlights(bufnr, hl, flattened)
|
|
|
|
-- Add details and lineno virtual text.
|
|
if cfg.o.outline_items.show_symbol_details then
|
|
for index, value in ipairs(details) do
|
|
vim.api.nvim_buf_set_extmark(bufnr, ns, index - 1, -1, {
|
|
virt_text = { { value, 'OutlineDetails' } },
|
|
virt_text_pos = 'eol',
|
|
hl_mode = 'combine',
|
|
})
|
|
end
|
|
end
|
|
if cfg.o.outline_items.show_symbol_lineno then
|
|
-- Line numbers are left padded, right aligned, positioned at the leftmost
|
|
-- column
|
|
-- TODO: Fix lineno not appearing if text in line is truncated on the right
|
|
-- due to narrow window, after nvim fixes virt_text_hide.
|
|
for index, value in ipairs(linenos) do
|
|
vim.api.nvim_buf_set_extmark(bufnr, ns, index - 1, -1, {
|
|
virt_text = { { value, 'OutlineLineno' } },
|
|
virt_text_pos = 'overlay',
|
|
virt_text_win_col = 0,
|
|
-- When hide_cursor + cursorline enabled, we want the lineno to also
|
|
-- take on the cursorline background so wherever the cursor is, it
|
|
-- appears blended. We want 'replace' even for `hide_cursor=false
|
|
-- cursorline=true` because vim's native line numbers do not get
|
|
-- highlighted by cursorline.
|
|
hl_mode = (cfg.o.outline_window.hide_cursor and 'combine') or 'replace',
|
|
})
|
|
end
|
|
end
|
|
|
|
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
|
|
-- linenos together with each line?
|
|
-- That is,
|
|
-- 1. { call nvim_buf_set_lines once for all lines }
|
|
-- + { O(n) for each of highlights, details, and linenos }
|
|
--OR
|
|
-- 2. { call nvim_buf_set_lines for each line }
|
|
-- + { O(1) for each of highlight/detail/lineno the same iteration }
|
|
-- It appears that for highlight/detail/lineno, the number of calls to nvim API
|
|
-- is the same, only 3 extra tables in memory for (1). Where as for (2) you
|
|
-- have to call nvim_buf_set_lines n times (each line) rather than add lines
|
|
-- all at once, saving only the need of 1 extra table (lines table) in memory.
|
|
|
|
return M
|