MAJOR: Complete rewrite of outline parsing into buffer lines
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.
This commit is contained in:
@@ -563,6 +563,11 @@ require'outline'
|
||||
- You can customize the split command used for creating the outline window split
|
||||
using `outline_window.split_command`, such as `"topleft vsp"`. See `:h windows`
|
||||
|
||||
- Is the outline window too slow when first opening a file? This is usually due
|
||||
to the LSP not being ready when you open outline, hence we have to wait for the
|
||||
LSP response before the outline can be shown. If LSP is ready generally the
|
||||
outline latency is almost negligible.
|
||||
|
||||
## Recipes
|
||||
|
||||
Behaviour you may want to achieve and the combination of configuration options
|
||||
|
||||
@@ -2,6 +2,8 @@ local vim = vim
|
||||
|
||||
local M = {}
|
||||
|
||||
-- TODO: Types for config schema
|
||||
|
||||
M.defaults = {
|
||||
guides = {
|
||||
enabled = true,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
local M = {}
|
||||
local cfg = require 'outline.config'
|
||||
|
||||
M.is_foldable = function(node)
|
||||
---@param node outline.SymbolNode|outline.FlatSymbolNode
|
||||
function M.is_foldable(node)
|
||||
return node.children and #node.children > 0
|
||||
end
|
||||
|
||||
local get_default_folded = function(depth)
|
||||
---@param depth integer
|
||||
local function get_default_folded(depth)
|
||||
local fold_past = cfg.o.symbol_folding.autofold_depth
|
||||
if not fold_past then
|
||||
return false
|
||||
@@ -14,7 +16,8 @@ local get_default_folded = function(depth)
|
||||
end
|
||||
end
|
||||
|
||||
M.is_folded = function(node)
|
||||
---@param node outline.SymbolNode|outline.FlatSymbolNode
|
||||
function M.is_folded(node)
|
||||
if node.folded ~= nil then
|
||||
return node.folded
|
||||
elseif node.hovered and cfg.o.symbol_folding.auto_unfold_hover then
|
||||
|
||||
@@ -43,7 +43,9 @@ end
|
||||
-- STATE
|
||||
-------------------------
|
||||
M.state = {
|
||||
---@type outline.SymbolNode[]
|
||||
outline_items = {},
|
||||
---@type outline.FlatSymbolNode[]
|
||||
flattened_outline_items = {},
|
||||
code_win = 0,
|
||||
-- In case unhide_cursor was called before hide_cursor for _some_ reason,
|
||||
@@ -56,10 +58,10 @@ local function wipe_state()
|
||||
end
|
||||
|
||||
local function _update_lines()
|
||||
M.state.flattened_outline_items = parser.flatten(M.state.outline_items)
|
||||
writer.parse_and_write(M.view.bufnr, M.state.flattened_outline_items)
|
||||
M.state.flattened_outline_items = writer.make_outline(M.view.bufnr, M.state.outline_items)
|
||||
end
|
||||
|
||||
---@param items outline.SymbolNode[]
|
||||
local function _merge_items(items)
|
||||
utils.merge_items_rec(
|
||||
{ children = items },
|
||||
@@ -90,11 +92,13 @@ end
|
||||
|
||||
M._refresh = utils.debounce(__refresh, 100)
|
||||
|
||||
---@return outline.FlatSymbolNode
|
||||
function M._current_node()
|
||||
local current_line = vim.api.nvim_win_get_cursor(M.view.winnr)[1]
|
||||
return M.state.flattened_outline_items[current_line]
|
||||
end
|
||||
|
||||
---@param change_focus boolean
|
||||
function M.__goto_location(change_focus)
|
||||
local node = M._current_node()
|
||||
vim.api.nvim_win_set_cursor(
|
||||
@@ -107,7 +111,9 @@ function M.__goto_location(change_focus)
|
||||
end
|
||||
end
|
||||
|
||||
-- Wraps __goto_location and handles auto_close
|
||||
---Wraps __goto_location and handles auto_close.
|
||||
---@see __goto_location
|
||||
---@param change_focus boolean
|
||||
function M._goto_location(change_focus)
|
||||
M.__goto_location(change_focus)
|
||||
if change_focus and cfg.o.outline_window.auto_close then
|
||||
@@ -120,6 +126,7 @@ function M._goto_and_close()
|
||||
M.close_outline()
|
||||
end
|
||||
|
||||
---@param direction "up"|"down"
|
||||
function M._move_and_goto(direction)
|
||||
local move = direction == 'down' and 1 or -1
|
||||
local cur = vim.api.nvim_win_get_cursor(0)
|
||||
@@ -128,6 +135,8 @@ function M._move_and_goto(direction)
|
||||
M.__goto_location(false)
|
||||
end
|
||||
|
||||
---@param move_cursor boolean
|
||||
---@param node_index integer Index for M.state.flattened_outline_items
|
||||
function M._toggle_fold(move_cursor, node_index)
|
||||
local node = M.state.flattened_outline_items[node_index] or M._current_node()
|
||||
local is_folded = folding.is_folded(node)
|
||||
@@ -186,6 +195,9 @@ local function setup_buffer_autocmd()
|
||||
end
|
||||
end
|
||||
|
||||
---@param folded boolean
|
||||
---@param move_cursor? boolean
|
||||
---@param node_index? integer
|
||||
function M._set_folded(folded, move_cursor, node_index)
|
||||
local node = M.state.flattened_outline_items[node_index] or M._current_node()
|
||||
local changed = (folded ~= folding.is_folded(node))
|
||||
@@ -212,6 +224,7 @@ function M._set_folded(folded, move_cursor, node_index)
|
||||
end
|
||||
end
|
||||
|
||||
---@param nodes outline.SymbolNode[]
|
||||
function M._toggle_all_fold(nodes)
|
||||
nodes = nodes or M.state.outline_items
|
||||
local folded = true
|
||||
@@ -226,6 +239,8 @@ function M._toggle_all_fold(nodes)
|
||||
M._set_all_folded(not folded, nodes)
|
||||
end
|
||||
|
||||
---@param folded boolean|nil
|
||||
---@param nodes? outline.SymbolNode[]
|
||||
function M._set_all_folded(folded, nodes)
|
||||
local stack = { nodes or M.state.outline_items }
|
||||
|
||||
@@ -242,6 +257,7 @@ function M._set_all_folded(folded, nodes)
|
||||
_update_lines()
|
||||
end
|
||||
|
||||
---@param winnr? integer Window number of code window
|
||||
function M._highlight_current_item(winnr)
|
||||
local has_provider = M.has_provider()
|
||||
local has_outline_open = M.view:is_open()
|
||||
@@ -373,6 +389,8 @@ local function setup_keymaps(bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param response table?
|
||||
---@param opts outline.OutlineOpts?
|
||||
local function handler(response, opts)
|
||||
if response == nil or type(response) ~= 'table' or M.view:is_open() then
|
||||
return
|
||||
@@ -394,9 +412,7 @@ local function handler(response, opts)
|
||||
local items = parser.parse(response)
|
||||
|
||||
M.state.outline_items = items
|
||||
M.state.flattened_outline_items = parser.flatten(items)
|
||||
|
||||
writer.parse_and_write(M.view.bufnr, M.state.flattened_outline_items)
|
||||
M.state.flattened_outline_items = writer.make_outline(M.view.bufnr, items)
|
||||
|
||||
M._highlight_current_item(M.state.code_win)
|
||||
|
||||
@@ -405,9 +421,12 @@ local function handler(response, opts)
|
||||
end
|
||||
end
|
||||
|
||||
---@class outline.OutlineOpts
|
||||
---@field focus_outline boolean
|
||||
|
||||
---Set position of outline window to match cursor position in code, return
|
||||
---whether the window is just newly opened (previously not open).
|
||||
---@param opts table? Field `focus_outline` = `false` or `nil` means don't focus on outline window after following cursor. If opts is not provided, focus will be on outline window after following cursor.
|
||||
---@param opts outline.OutlineOpts? Field `focus_outline` = `false` or `nil` means don't focus on outline window after following cursor. If opts is not provided, focus will be on outline window after following cursor.
|
||||
---@return boolean ok Whether it was successful. If ok=false, either the outline window is not open or the code window cannot be found.
|
||||
function M.follow_cursor(opts)
|
||||
if not M.view:is_open() then
|
||||
@@ -449,7 +468,8 @@ end
|
||||
|
||||
---Toggle the outline window, and return whether the outline window is open
|
||||
---after this operation.
|
||||
---@param opts table? Table of options, @see open_outline
|
||||
---@see open_outline
|
||||
---@param opts outline.OutlineOpts? Table of options
|
||||
---@return boolean is_open Whether outline window is open
|
||||
function M.toggle_outline(opts)
|
||||
if M.view:is_open() then
|
||||
@@ -471,7 +491,7 @@ local function _cmd_toggle_outline(opts)
|
||||
end
|
||||
|
||||
---Open the outline window.
|
||||
---@param opts table? Field focus_outline=false means don't focus on outline window after opening. If opts is not provided, focus will be on outline window after opening.
|
||||
---@param opts outline.OutlineOpts? Field focus_outline=false means don't focus on outline window after opening. If opts is not provided, focus will be on outline window after opening.
|
||||
function M.open_outline(opts)
|
||||
if not opts then
|
||||
opts = { focus_outline = true }
|
||||
|
||||
@@ -7,24 +7,44 @@ local folding = require 'outline.folding'
|
||||
|
||||
local M = {}
|
||||
|
||||
---Parses result from LSP into a table of symbols
|
||||
---@class outline.SymbolNode
|
||||
---@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
|
||||
|
||||
---Parses result from LSP into a reorganized tree of symbols (not flattened,
|
||||
-- simply reoganized by merging each property table from the arguments into a
|
||||
-- table for each symbol)
|
||||
---@param result table The result from a language server.
|
||||
---@param depth number? The current depth of the symbol in the hierarchy.
|
||||
---@param hierarchy table? A table of booleans which tells if a symbols parent was the last in its group.
|
||||
---@param parent table? A reference to the current symbol's parent in the function's recursion
|
||||
---@return table
|
||||
---@return outline.SymbolNode[]
|
||||
local function parse_result(result, depth, hierarchy, parent)
|
||||
local ret = {}
|
||||
|
||||
for index, value in pairs(result) do
|
||||
if not cfg.is_symbol_blacklisted(symbols.kinds[value.kind]) then
|
||||
-- the hierarchy is basically a table of booleans which tells whether
|
||||
-- the parent was the last in its group or not
|
||||
-- the hierarchy is basically a table of booleans which
|
||||
-- tells whether the parent was the last in its group or
|
||||
-- not
|
||||
local hir = hierarchy or {}
|
||||
-- how many parents this node has, 1 is the lowest value because its
|
||||
-- easier to work it
|
||||
local level = depth or 1
|
||||
-- whether this node is the last in its group
|
||||
-- whether this node is the last (~born~) in its siblings
|
||||
local isLast = index == #result
|
||||
|
||||
local selectionRange = lsp_utils.get_selection_range(value)
|
||||
@@ -44,6 +64,7 @@ local function parse_result(result, depth, hierarchy, parent)
|
||||
isLast = isLast,
|
||||
hierarchy = hir,
|
||||
parent = parent,
|
||||
traversal_child = 1,
|
||||
}
|
||||
|
||||
table.insert(ret, node)
|
||||
@@ -54,6 +75,8 @@ local function parse_result(result, depth, hierarchy, parent)
|
||||
local child_hir = t_utils.array_copy(hir)
|
||||
table.insert(child_hir, isLast)
|
||||
children = parse_result(value.children, level + 1, child_hir, node)
|
||||
else
|
||||
value.children = {}
|
||||
end
|
||||
|
||||
node.children = children
|
||||
@@ -62,195 +85,48 @@ local function parse_result(result, depth, hierarchy, parent)
|
||||
return ret
|
||||
end
|
||||
|
||||
---Parses the response from lsp request 'textDocument/documentSymbol' using buf_request_all
|
||||
---Sorts and reorganizes the response from lsp request
|
||||
--'textDocument/documentSymbol', buf_request_all.
|
||||
---Used when refreshing and setting up new symbols
|
||||
---@param response table The result from buf_request_all
|
||||
---@return table outline items
|
||||
---@return outline.SymbolNode[]
|
||||
function M.parse(response)
|
||||
local sorted = lsp_utils.sort_symbols(response)
|
||||
|
||||
return parse_result(sorted, nil, nil)
|
||||
end
|
||||
|
||||
function M.flatten(outline_items, ret, depth)
|
||||
depth = depth or 1
|
||||
ret = ret or {}
|
||||
for _, value in ipairs(outline_items) do
|
||||
table.insert(ret, value)
|
||||
value.line_in_outline = #ret
|
||||
if value.children ~= nil and not folding.is_folded(value) then
|
||||
M.flatten(value.children, ret, depth + 1)
|
||||
end
|
||||
---Iterator that traverses the tree parent first before children, returning each node.
|
||||
-- Essentailly 'flatten' items, but returns an iterator.
|
||||
---@param items outline.SymbolNode[] Tree of symbols parsed by parse_result
|
||||
function M.preorder_iter(items)
|
||||
local node = { children = items, traversal_child = 1, depth = 1, folded = false }
|
||||
local prev
|
||||
local visited = {}
|
||||
|
||||
return function()
|
||||
while node do
|
||||
if node.name and not visited[node] then
|
||||
visited[node] = true
|
||||
return node
|
||||
end
|
||||
|
||||
-- if depth == 1 then
|
||||
-- for index, value in ipairs(ret) do
|
||||
-- value.line_in_outline = index
|
||||
-- end
|
||||
-- end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.get_lines(flattened_outline_items)
|
||||
local lines = {}
|
||||
local hl_info = {}
|
||||
local guide_hl_info = {}
|
||||
local lineno_max = 0
|
||||
|
||||
for node_line, node in ipairs(flattened_outline_items) do
|
||||
local depth = node.depth
|
||||
local marker_space = (cfg.o.symbol_folding.markers and 1) or 0
|
||||
|
||||
local line = t_utils.str_to_table(string.rep(' ', depth + marker_space))
|
||||
local running_length = 1
|
||||
|
||||
if node.range_start+1 > lineno_max then
|
||||
lineno_max = node.range_start+1
|
||||
if
|
||||
node.children and node.traversal_child <= #node.children
|
||||
and not folding.is_folded(node)
|
||||
then
|
||||
prev = node
|
||||
if node.children[node.traversal_child] then
|
||||
node.children[node.traversal_child].parent_node = node
|
||||
node = node.children[node.traversal_child]
|
||||
end
|
||||
|
||||
local function add_guide_hl(from, to)
|
||||
table.insert(guide_hl_info, {
|
||||
node_line,
|
||||
from,
|
||||
to,
|
||||
'OutlineGuides',
|
||||
})
|
||||
end
|
||||
|
||||
for index, _ in ipairs(line) do
|
||||
if cfg.o.guides.enabled then
|
||||
local guide_markers = cfg.o.guides.markers
|
||||
if index == 1 then
|
||||
line[index] = ''
|
||||
-- if index is last, add a bottom marker if current item is last,
|
||||
-- else add a middle marker
|
||||
elseif index == #line then
|
||||
-- add fold markers
|
||||
local fold_markers = cfg.o.symbol_folding.markers
|
||||
if fold_markers and folding.is_foldable(node) then
|
||||
if folding.is_folded(node) then
|
||||
line[index] = fold_markers[1]
|
||||
prev.traversal_child = prev.traversal_child + 1
|
||||
else
|
||||
line[index] = fold_markers[2]
|
||||
end
|
||||
|
||||
add_guide_hl(
|
||||
running_length,
|
||||
running_length + vim.fn.strlen(line[index]) - 1
|
||||
)
|
||||
|
||||
-- the root level has no vertical markers
|
||||
elseif depth > 1 then
|
||||
if node.isLast then
|
||||
line[index] = guide_markers.bottom
|
||||
add_guide_hl(
|
||||
running_length,
|
||||
running_length + vim.fn.strlen(guide_markers.bottom) - 1
|
||||
)
|
||||
else
|
||||
line[index] = guide_markers.middle
|
||||
add_guide_hl(
|
||||
running_length,
|
||||
running_length + vim.fn.strlen(guide_markers.middle) - 1
|
||||
)
|
||||
end
|
||||
end
|
||||
-- else if the parent was not the last in its group, add a
|
||||
-- vertical marker because there are items under us and we need
|
||||
-- to point to those
|
||||
elseif not node.hierarchy[index] and depth > 1 then
|
||||
line[index + marker_space] = guide_markers.vertical
|
||||
add_guide_hl(
|
||||
running_length - 1 + 2 * marker_space,
|
||||
running_length
|
||||
+ vim.fn.strlen(guide_markers.vertical)
|
||||
- 1
|
||||
+ 2 * marker_space
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
line[index] = line[index] .. ' '
|
||||
|
||||
running_length = running_length + vim.fn.strlen(line[index])
|
||||
end
|
||||
|
||||
line[1] = ''
|
||||
local final_prefix = line
|
||||
|
||||
local string_prefix = t_utils.table_to_str(final_prefix)
|
||||
|
||||
table.insert(lines, string_prefix .. node.icon .. ' ' .. node.name)
|
||||
|
||||
local hl_start = #string_prefix
|
||||
local hl_end = #string_prefix + #node.icon
|
||||
local hl_type = cfg.o.symbols.icons[symbols.kinds[node.kind]].hl
|
||||
table.insert(hl_info, { node_line, hl_start, hl_end, hl_type })
|
||||
|
||||
node.prefix_length = #string_prefix + #node.icon + 1
|
||||
end
|
||||
|
||||
local final_hl = {}
|
||||
if cfg.o.outline_items.show_symbol_lineno then
|
||||
-- Width of the highest lineno value
|
||||
local max_width = #tostring(lineno_max)
|
||||
-- Padded prefix to the right of lineno for better readability if linenos
|
||||
-- get more than 2 digits.
|
||||
local prefix = string.rep(' ', math.max(2, max_width)+1)
|
||||
-- Offset to hl_info due to adding lineno on the left of each symbol line
|
||||
local total_offset = #prefix
|
||||
for i, node in ipairs(flattened_outline_items) do
|
||||
lines[i] = prefix .. lines[i]
|
||||
table.insert(final_hl, {
|
||||
hl_info[i][1], -- node_line
|
||||
hl_info[i][2] + total_offset, -- start
|
||||
hl_info[i][3] + total_offset, -- end
|
||||
hl_info[i][4] -- type
|
||||
})
|
||||
node.prefix_length = node.prefix_length + total_offset
|
||||
end
|
||||
if cfg.o.guides.enabled then
|
||||
for _, hl in ipairs(guide_hl_info) do
|
||||
table.insert(final_hl, {
|
||||
hl[1],
|
||||
hl[2] + total_offset,
|
||||
hl[3] + total_offset,
|
||||
hl[4]
|
||||
})
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Merge lists hl_info and guide_hl_info
|
||||
final_hl = hl_info
|
||||
if cfg.o.guides.enabled then
|
||||
for _, hl in ipairs(guide_hl_info) do
|
||||
table.insert(final_hl, hl)
|
||||
node.traversal_child = 1
|
||||
node = node.parent_node
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines, final_hl
|
||||
end
|
||||
|
||||
function M.get_details(flattened_outline_items)
|
||||
local lines = {}
|
||||
for _, value in ipairs(flattened_outline_items) do
|
||||
table.insert(lines, value.detail or '')
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
function M.get_lineno(flattened_outline_items)
|
||||
local lines = {}
|
||||
local max = 0
|
||||
for _, value in ipairs(flattened_outline_items) do
|
||||
local line = value.range_start+1
|
||||
if line > max then
|
||||
max = line
|
||||
end
|
||||
-- Not padded here
|
||||
table.insert(lines, tostring(line))
|
||||
end
|
||||
return lines, max
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -26,6 +26,7 @@ function M.has_provider()
|
||||
end
|
||||
|
||||
---@param on_symbols function
|
||||
---@param opts outline.OutlineOpts?
|
||||
---@return boolean found_provider
|
||||
function M.request_symbols(on_symbols, opts)
|
||||
for _, value in ipairs(providers) do
|
||||
|
||||
@@ -21,6 +21,7 @@ function M.setup_highlights()
|
||||
-- Setup the OutlineCurrent highlight group if it hasn't been done already by
|
||||
-- a theme or manually set
|
||||
if vim.fn.hlexists 'OutlineCurrent' == 0 then
|
||||
-- TODO: Use nvim_get_hl
|
||||
local cline_hl = vim.api.nvim_get_hl_by_name('CursorLine', true)
|
||||
local string_hl = vim.api.nvim_get_hl_by_name('String', true)
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ local function range_compare(a, b)
|
||||
return (a.line < b.line) or (a.line == b.line and a.character < b.character)
|
||||
end
|
||||
|
||||
--- Sorts the result from LSP by where the symbols start.
|
||||
---Sort the result from LSP by where the symbols start.
|
||||
---Recursively sorts all children of each symbol
|
||||
function M.sort_symbols(symbols)
|
||||
table.sort(symbols, function(a, b)
|
||||
return range_compare(
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
local symbols = require 'outline.symbols'
|
||||
local parser = require 'outline.parser'
|
||||
local cfg = require('outline.config')
|
||||
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 function is_buffer_outline(bufnr)
|
||||
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
|
||||
@@ -13,23 +25,9 @@ local function is_buffer_outline(bufnr)
|
||||
return string.match(name, 'OUTLINE') ~= nil and ft == 'Outline'
|
||||
end
|
||||
|
||||
local hlns = vim.api.nvim_create_namespace 'outline-icon-highlight'
|
||||
|
||||
function M.write_outline(bufnr, lines)
|
||||
if not is_buffer_outline(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
lines = vim.tbl_map(function(line)
|
||||
lines, _ = string.gsub(line, "\n", " ")
|
||||
return lines
|
||||
end, 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)
|
||||
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)
|
||||
@@ -42,54 +40,17 @@ function M.add_highlights(bufnr, hl_info, nodes)
|
||||
hl_end
|
||||
)
|
||||
end
|
||||
|
||||
M.add_hover_highlights(bufnr, nodes)
|
||||
end
|
||||
|
||||
local ns = vim.api.nvim_create_namespace 'outline-virt-text'
|
||||
|
||||
function M.write_details(bufnr, lines)
|
||||
if not is_buffer_outline(bufnr) then
|
||||
return
|
||||
end
|
||||
if not cfg.o.outline_items.show_symbol_details then
|
||||
return
|
||||
end
|
||||
|
||||
for index, value in ipairs(lines) 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
|
||||
|
||||
function M.write_lineno(bufnr, lines, max)
|
||||
if not is_buffer_outline(bufnr) then
|
||||
return
|
||||
end
|
||||
if not cfg.o.outline_items.show_symbol_lineno then
|
||||
return
|
||||
end
|
||||
local maxwidth = #tostring(max)
|
||||
|
||||
for index, value in ipairs(lines) do
|
||||
local leftpad = string.rep(' ', maxwidth-#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
|
||||
|
||||
---@param bufnr integer
|
||||
local function clear_virt_text(bufnr)
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1)
|
||||
end
|
||||
|
||||
M.add_hover_highlights = function(bufnr, nodes)
|
||||
---@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
|
||||
@@ -101,7 +62,6 @@ M.add_hover_highlights = function(bufnr, nodes)
|
||||
goto continue
|
||||
end
|
||||
|
||||
local marker_fac = (cfg.o.symbol_folding.markers and 1) or 0
|
||||
if node.prefix_length then
|
||||
ui.add_hover_highlight(
|
||||
bufnr,
|
||||
@@ -113,18 +73,207 @@ M.add_hover_highlights = function(bufnr, nodes)
|
||||
end
|
||||
end
|
||||
|
||||
-- runs the whole writing routine where the text is cleared, new data is parsed
|
||||
-- and then written
|
||||
function M.parse_and_write(bufnr, flattened_outline_items)
|
||||
local lines, hl_info = parser.get_lines(flattened_outline_items)
|
||||
M.write_outline(bufnr, lines)
|
||||
---@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)
|
||||
local details = parser.get_details(flattened_outline_items)
|
||||
local lineno, lineno_max = parser.get_lineno(flattened_outline_items)
|
||||
M.add_highlights(bufnr, hl_info, flattened_outline_items)
|
||||
M.write_details(bufnr, details)
|
||||
M.write_lineno(bufnr, lineno, lineno_max)
|
||||
|
||||
---@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
|
||||
|
||||
Reference in New Issue
Block a user