Scope: - Parsing of symbol tree - Producing the flattened tree - Producing the lines shown in the outline based on the symbol tree - API of exported functions for parser.lua and writer.lua Note that the formatting of the outline remains the same as before. Fixes: - Guide highlights sometimes cover fold marker areas (may be related to the issue brought up by @silvercircle on reddit) - Guide highlights do not work when using guide markers of different widths than the default (such as setting all markers to ascii chars) All of these issues are now fixed after integrating the a parser algorithm. This commit introduces: 1. A better algorithm for flattening & parsing the tree in one go 2. `OutlineFoldMarker` highlight group 3. Fixed inconsistent highlighting of guides and legacy (somewhat weaker code), through (1). 4. Minor performance improvements 5. Type hints for the symbol tree 6. Removed several functions from writer.lua and parser.lua due to them being merged into writer.make_outline This can be seen as a breaking change because functions that were exported had altered behaviours. However I doubt these functions actually have any critical use outside of this plugin, hence it isn't really a breaking change as the user-experience remains the same. The extraneous left padding on the outline window is now a relic of the past 🎉 The old implementation, parser.get_lines used a flattened tree and was inefficient, full of off-by-one corrections. While trying to look for bug fixes in that function I realized it's the sort of "if it works, don't touch it" portion of code. Hence, I figured a complete rewrite is necessary. Now, the function responsible for making the outline lines lives at writer.make_outline. Building the flattened tree, getting lines, details and linenos are now done in one go. This is a tradeoff between modular design and efficiency. parser.lua still serve important purposes: - local parse_result function converts the hierarchical tables from provider into a nested form tree, used everywhere in outline.nvim. The type hint of the return value is now defined -- outline.SymbolNode - preorder_iter is an iterator that traverses the aforementioned tree in pre-order style. First the parents, all the childrens, and so on until the last node of the root tree. This is used in writer.make_outline to abstract a way the traversal code from the code of making the lines. Thanks to stack overflow I did not have to consult a DS book to figure out the cleanest way of this traversal method without using recursion. This, of course, closes #14 on github. Note that another minor 'breaking' change is that previously, hl for the guides where grouped per-character, now they are grouped together for all the guide markers in the same line. This should not be a problem for those who only style the fg color for guide hl. However, if you're styling the bg color, they will now take on that bg collectively rather than individually. This change eliminates future maintenance burden because controlling per-character guide highlights requires careful avoidance of off-by-one errors. I have tested most common features to work as before. I may have missed particular edge cases. Please take note of "scope" at the top of this commit message.
280 lines
8.8 KiB
Lua
280 lines
8.8 KiB
Lua
local symbols = require 'outline.symbols'
|
|
local parser = require 'outline.parser'
|
|
local cfg = require 'outline.config'
|
|
local ui = require 'outline.ui'
|
|
local t_utils = require 'outline.utils.table'
|
|
local folding = require 'outline.folding'
|
|
|
|
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
|
|
|
|
---@class outline.FlatSymbolNode
|
|
---@field name string
|
|
---@field depth integer
|
|
---@field parent outline.SymbolNode
|
|
---@field deprecated boolean
|
|
---@field kind integer|string
|
|
---@field icon string
|
|
---@field detail string
|
|
---@field line integer
|
|
---@field character integer
|
|
---@field range_start integer
|
|
---@field range_end integer
|
|
---@field isLast boolean
|
|
---@field hierarchy boolean
|
|
---@field children? outline.SymbolNode[]
|
|
---@field traversal_child integer
|
|
---@field line_in_outline integer
|
|
---@field prefix_length integer
|
|
---@field hovered boolean
|
|
---@field folded boolean
|
|
|
|
---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
|
|
---@return outline.FlatSymbolNode[]? flattened_items No return value if bufnr is invalid
|
|
function M.make_outline(bufnr, items)
|
|
if not M.is_buffer_outline(bufnr) then
|
|
return
|
|
end
|
|
|
|
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 = ""
|
|
-- FIXME: Why is the +1 at the end needed? (Otherwise numbers are misaligned)
|
|
local lineno_max_width = #tostring(vim.api.nvim_buf_line_count(bufnr) - 1) + 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 + lineno_offset,
|
|
to + lineno_offset,
|
|
"OutlineGuides"
|
|
})
|
|
end
|
|
|
|
local function add_fold_hl(from, to)
|
|
table.insert(hl, {
|
|
#flattened,
|
|
from + lineno_offset,
|
|
to + lineno_offset,
|
|
"OutlineFoldMarker"
|
|
})
|
|
end
|
|
|
|
local guide_markers = cfg.o.guides.markers
|
|
local fold_markers = cfg.o.symbol_folding.markers
|
|
|
|
for node in parser.preorder_iter(items) do
|
|
table.insert(flattened, node)
|
|
node.line_in_outline = #flattened
|
|
table.insert(details, node.detail or '')
|
|
table.insert(linenos, tostring(node.range_start+1))
|
|
|
|
-- 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, ' ')
|
|
|
|
-- 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, #pref_str - fold_marker_width)
|
|
if fold_marker_width > 0 then
|
|
add_fold_hl(#pref_str - fold_marker_width, #pref_str + 1)
|
|
end
|
|
|
|
local line = lineno_prefix..pref_str..' '..node.icon..' '..node.name
|
|
|
|
-- Highlight for the icon ✨
|
|
local hl_start = #pref_str + #lineno_prefix + 1 -- Start from icon col
|
|
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
|
|
for index, value in ipairs(linenos) do
|
|
local leftpad = string.rep(' ', lineno_max_width-#value)
|
|
vim.api.nvim_buf_set_extmark(bufnr, ns, index - 1, -1, {
|
|
virt_text = { {leftpad..value, 'OutlineLineno' } },
|
|
virt_text_pos = 'overlay',
|
|
virt_text_win_col = 0,
|
|
hl_mode = 'combine',
|
|
})
|
|
end
|
|
end
|
|
|
|
return flattened
|
|
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
|