diff --git a/README.md b/README.md index dfe2035..125ce62 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lua/outline/config.lua b/lua/outline/config.lua index 1a89432..6c6c36b 100644 --- a/lua/outline/config.lua +++ b/lua/outline/config.lua @@ -2,6 +2,8 @@ local vim = vim local M = {} +-- TODO: Types for config schema + M.defaults = { guides = { enabled = true, diff --git a/lua/outline/folding.lua b/lua/outline/folding.lua index 0a2d0e5..75212c7 100644 --- a/lua/outline/folding.lua +++ b/lua/outline/folding.lua @@ -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 diff --git a/lua/outline/init.lua b/lua/outline/init.lua index ab44fa0..40f38d1 100644 --- a/lua/outline/init.lua +++ b/lua/outline/init.lua @@ -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 } diff --git a/lua/outline/parser.lua b/lua/outline/parser.lua index dc15e01..2139f16 100644 --- a/lua/outline/parser.lua +++ b/lua/outline/parser.lua @@ -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 - 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 = {} - -- if depth == 1 then - -- for index, value in ipairs(ret) do - -- value.line_in_outline = index - -- end - -- end + return function() + while node do + if node.name and not visited[node] then + visited[node] = true + return node + 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 - 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] - 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 - ) + 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 - 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) + prev.traversal_child = prev.traversal_child + 1 + else + 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 diff --git a/lua/outline/providers/init.lua b/lua/outline/providers/init.lua index 343446a..e667b36 100644 --- a/lua/outline/providers/init.lua +++ b/lua/outline/providers/init.lua @@ -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 diff --git a/lua/outline/ui.lua b/lua/outline/ui.lua index 544e48c..f1e9f62 100644 --- a/lua/outline/ui.lua +++ b/lua/outline/ui.lua @@ -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) diff --git a/lua/outline/utils/lsp_utils.lua b/lua/outline/utils/lsp_utils.lua index dc2e485..887c835 100644 --- a/lua/outline/utils/lsp_utils.lua +++ b/lua/outline/utils/lsp_utils.lua @@ -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( diff --git a/lua/outline/writer.lua b/lua/outline/writer.lua index 04ede44..631d0c4 100644 --- a/lua/outline/writer.lua +++ b/lua/outline/writer.lua @@ -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