Skip to content

Commit

Permalink
MAJOR: Complete rewrite of outline parsing into buffer lines
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hedyhli committed Nov 13, 2023
1 parent bc2b0ff commit 66aecc7
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 266 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lua/outline/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ local vim = vim

local M = {}

-- TODO: Types for config schema

M.defaults = {
guides = {
enabled = true,
Expand Down
9 changes: 6 additions & 3 deletions lua/outline/folding.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
38 changes: 29 additions & 9 deletions lua/outline/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 },
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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 }

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand Down
Loading

0 comments on commit 66aecc7

Please sign in to comment.