Merge pull request #142 from mxsdev/folding

Add folding
This commit is contained in:
sim
2022-08-17 17:08:43 -07:00
committed by GitHub
10 changed files with 339 additions and 71 deletions

View File

@@ -43,6 +43,9 @@ local opts = {
show_relative_numbers = false,
show_symbol_details = true,
preview_bg_highlight = 'Pmenu',
autofold_depth = nil,
auto_unfold_hover = true,
fold_markers = { '', '' },
keymaps = { -- These keymaps can be a string or a table for multiple keys
close = {"<Esc>", "q"},
goto_location = "<Cr>",
@@ -51,6 +54,11 @@ local opts = {
toggle_preview = "K",
rename_symbol = "r",
code_actions = "a",
fold = "h",
unfold = "l",
fold_all = "W",
unfold_all = "E",
fold_reset = "R",
},
lsp_blacklist = {},
symbol_blacklist = {},
@@ -103,6 +111,9 @@ local opts = {
| symbols | Icon and highlight config for symbol icons | table (dictionary) | scroll up |
| lsp_blacklist | Which lsp clients to ignore | table (array) | {} |
| symbol_blacklist | Which symbols to ignore ([possible values](./lua/symbols-outline/symbols.lua)) | table (array) | {} |
| autofold_depth | Depth past which nodes will be folded by default | int | nil |
| auto_unfold_hover | Automatically unfold hovered symbol | boolean | true |
| fold_markers | Markers to denote foldable symbol's status | table (array) | { '', '' } |
## Commands
@@ -123,6 +134,11 @@ local opts = {
| K | Toggles the current symbol preview |
| r | Rename symbol |
| a | Code actions |
| h | Unfold symbol |
| l | Fold symbol |
| W | Fold all symbols |
| E | Unfold all symbols |
| R | Reset all folding |
| ? | Show help message |
## Highlights

View File

@@ -5,11 +5,14 @@ local writer = require 'symbols-outline.writer'
local config = require 'symbols-outline.config'
local utils = require 'symbols-outline.utils.init'
local View = require 'symbols-outline.view'
local folding = require 'symbols-outline.folding'
local M = {}
local function setup_global_autocmd()
if config.options.highlight_hovered_item then
if
config.options.highlight_hovered_item or config.options.auto_unfold_hover
then
vim.api.nvim_create_autocmd('CursorHold', {
pattern = '*',
callback = function()
@@ -63,6 +66,18 @@ local function wipe_state()
M.state = { outline_items = {}, flattened_outline_items = {}, code_win = 0 }
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)
end
local function _merge_items(items)
utils.merge_items_rec(
{ children = items },
{ children = M.state.outline_items }
)
end
local function __refresh()
if M.view:is_open() then
local function refresh_handler(response)
@@ -71,12 +86,11 @@ local function __refresh()
end
local items = parser.parse(response)
_merge_items(items)
M.state.code_win = vim.api.nvim_get_current_win()
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)
_update_lines()
end
providers.request_symbols(refresh_handler)
@@ -85,9 +99,13 @@ end
M._refresh = utils.debounce(__refresh, 100)
local function goto_location(change_focus)
function M._current_node()
local current_line = vim.api.nvim_win_get_cursor(M.view.winnr)[1]
local node = M.state.flattened_outline_items[current_line]
return M.state.flattened_outline_items[current_line]
end
local function goto_location(change_focus)
local node = M._current_node()
vim.api.nvim_win_set_cursor(
M.state.code_win,
{ node.line + 1, node.character }
@@ -100,6 +118,45 @@ local function goto_location(change_focus)
end
end
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))
if folding.is_foldable(node) and changed then
node.folded = folded
if move_cursor then
vim.api.nvim_win_set_cursor(M.view.winnr, { node_index, 0 })
end
_update_lines()
elseif node.parent then
local parent_node =
M.state.flattened_outline_items[node.parent.line_in_outline]
if parent_node then
M._set_folded(
folded,
not parent_node.folded and folded,
parent_node.line_in_outline
)
end
end
end
function M._set_all_folded(folded, nodes)
nodes = nodes or M.state.outline_items
for _, node in ipairs(nodes) do
node.folded = folded
if node.children then
M._set_all_folded(folded, node.children)
end
end
_update_lines()
end
function M._highlight_current_item(winnr)
local has_provider = providers.has_provider()
@@ -127,26 +184,31 @@ function M._highlight_current_item(winnr)
local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1
local nodes = {}
for index, value in ipairs(M.state.flattened_outline_items) do
local leaf_node = nil
local cb = function(value)
value.hovered = nil
if
value.line == hovered_line
or (hovered_line > value.range_start and hovered_line < value.range_end)
then
value.line_in_outline = index
table.insert(nodes, value)
value.hovered = true
leaf_node = value
end
end
-- clear old highlight
ui.clear_hover_highlight(M.view.bufnr)
for _, value in ipairs(nodes) do
ui.add_hover_highlight(
M.view.bufnr,
value.line_in_outline - 1,
value.depth * 2
)
vim.api.nvim_win_set_cursor(M.view.winnr, { value.line_in_outline, 1 })
utils.items_dfs(cb, M.state.outline_items)
_update_lines()
if leaf_node then
for index, node in ipairs(M.state.flattened_outline_items) do
if node == leaf_node then
vim.api.nvim_win_set_cursor(M.view.winnr, { index, 1 })
break
end
end
end
end
@@ -166,9 +228,12 @@ local function setup_keymaps(bufnr)
map(
config.options.keymaps.hover_symbol,
require('symbols-outline.hover').show_hover
)
)
-- preview symbol
map(config.options.keymaps.toggle_preview, require('symbols-outline.preview').toggle)
map(
config.options.keymaps.toggle_preview,
require('symbols-outline.preview').toggle
)
-- rename symbol
map(
config.options.keymaps.rename_symbol,
@@ -188,6 +253,26 @@ local function setup_keymaps(bufnr)
map(config.options.keymaps.close, function()
M.view:close()
end)
-- fold selection
map(config.options.keymaps.fold, function()
M._set_folded(true)
end)
-- unfold selection
map(config.options.keymaps.unfold, function()
M._set_folded(false)
end)
-- fold all
map(config.options.keymaps.fold_all, function()
M._set_all_folded(true)
end)
-- unfold all
map(config.options.keymaps.unfold_all, function()
M._set_all_folded(false)
end)
-- fold reset
map(config.options.keymaps.fold_reset, function()
M._set_all_folded(nil)
end)
end
local function handler(response)

View File

@@ -16,6 +16,9 @@ M.defaults = {
show_symbol_details = true,
preview_bg_highlight = 'Pmenu',
winblend = 0,
autofold_depth = nil,
auto_unfold_hover = true,
fold_markers = { '', '' },
keymaps = { -- These keymaps can be a string or a table for multiple keys
close = { '<Esc>', 'q' },
goto_location = '<Cr>',
@@ -25,6 +28,11 @@ M.defaults = {
rename_symbol = 'r',
code_actions = 'a',
show_help = '?',
fold = 'h',
unfold = 'l',
fold_all = 'W',
unfold_all = 'E',
fold_reset = 'R',
},
lsp_blacklist = {},
symbol_blacklist = {},

View File

@@ -0,0 +1,27 @@
local M = {}
local config = require 'symbols-outline.config'
M.is_foldable = function(node)
return node.children and #node.children > 0
end
local get_default_folded = function(depth)
local fold_past = config.options.autofold_depth
if not fold_past then
return false
else
return depth >= fold_past
end
end
M.is_folded = function(node)
if node.folded ~= nil then
return node.folded
elseif node.hovered and config.options.auto_unfold_hover then
return false
else
return get_default_folded(node.depth)
end
end
return M

View File

@@ -25,7 +25,7 @@ function M.show_hover()
hover_params.bufnr,
'textDocument/hover',
hover_params,
---@diagnostic disable-next-line: param-type-mismatch
---@diagnostic disable-next-line: param-type-mismatch
function(_, result, _, config)
if not (result and result.contents) then
-- return { 'No information available' }

View File

@@ -2,6 +2,7 @@ local symbols = require 'symbols-outline.symbols'
local ui = require 'symbols-outline.ui'
local config = require 'symbols-outline.config'
local t_utils = require 'symbols-outline.utils.table'
local folding = require 'symbols-outline.folding'
local M = {}
@@ -9,8 +10,9 @@ local M = {}
---@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
local function parse_result(result, depth, hierarchy)
local function parse_result(result, depth, hierarchy, parent)
local ret = {}
for index, value in pairs(result) do
@@ -24,14 +26,6 @@ local function parse_result(result, depth, hierarchy)
-- whether this node is the last in its group
local isLast = index == #result
local children = nil
if value.children ~= nil then
-- copy by value because we dont want it messing with the hir table
local child_hir = t_utils.array_copy(hir)
table.insert(child_hir, isLast)
children = parse_result(value.children, level + 1, child_hir)
end
-- support SymbolInformation[]
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
local selectionRange = value.selectionRange
@@ -44,7 +38,7 @@ local function parse_result(result, depth, hierarchy)
range = value.location.range
end
table.insert(ret, {
local node = {
deprecated = value.deprecated,
kind = value.kind,
icon = symbols.icon_from_kind(value.kind),
@@ -54,11 +48,23 @@ local function parse_result(result, depth, hierarchy)
character = selectionRange.start.character,
range_start = range.start.line,
range_end = range['end'].line,
children = children,
depth = level,
isLast = isLast,
hierarchy = hir,
})
parent = parent,
}
table.insert(ret, node)
local children = nil
if value.children ~= nil then
-- copy by value because we dont want it messing with the hir table
local child_hir = t_utils.array_copy(hir)
table.insert(child_hir, isLast)
children = parse_result(value.children, level + 1, child_hir, node)
end
node.children = children
end
end
return ret
@@ -139,14 +145,23 @@ function M.parse(response)
return parse_result(sorted, nil, nil)
end
function M.flatten(outline_items, ret)
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)
if value.children ~= nil then
M.flatten(value.children, ret)
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
-- if depth == 1 then
-- for index, value in ipairs(ret) do
-- value.line_in_outline = index
-- end
-- end
return ret
end
@@ -155,7 +170,10 @@ function M.get_lines(flattened_outline_items)
local hl_info = {}
for node_line, node in ipairs(flattened_outline_items) do
local line = t_utils.str_to_table(string.rep(' ', node.depth))
local depth = node.depth
local marker_space = (config.options.fold_markers and 1) or 0
local line = t_utils.str_to_table(string.rep(' ', depth + marker_space))
local running_length = 1
local function add_guide_hl(from, to)
@@ -176,27 +194,46 @@ function M.get_lines(flattened_outline_items)
-- i f index is last, add a bottom marker if current item is last,
-- else add a middle marker
elseif index == #line then
if node.isLast then
line[index] = ui.markers.bottom
-- add fold markers
if config.options.fold_markers and folding.is_foldable(node) then
if folding.is_folded(node) then
line[index] = config.options.fold_markers[1]
else
line[index] = config.options.fold_markers[2]
end
add_guide_hl(
running_length,
running_length + vim.fn.strlen(ui.markers.bottom) - 1
)
else
line[index] = ui.markers.middle
add_guide_hl(
running_length,
running_length + vim.fn.strlen(ui.markers.middle) - 1
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] = ui.markers.bottom
add_guide_hl(
running_length,
running_length + vim.fn.strlen(ui.markers.bottom) - 1
)
else
line[index] = ui.markers.middle
add_guide_hl(
running_length,
running_length + vim.fn.strlen(ui.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] then
line[index] = ui.markers.vertical
elseif not node.hierarchy[index] and depth > 1 then
line[index + marker_space] = ui.markers.vertical
add_guide_hl(
running_length,
running_length + vim.fn.strlen(ui.markers.vertical) - 1
running_length - 1 + 2 * marker_space,
running_length
+ vim.fn.strlen(ui.markers.vertical)
- 1
+ 2 * marker_space
)
end
end
@@ -216,6 +253,8 @@ function M.get_lines(flattened_outline_items)
local hl_end = #string_prefix + #node.icon
local hl_type = config.options.symbols[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
return lines, hl_info
end

View File

@@ -8,15 +8,12 @@ function M.should_use_provider(bufnr)
end
function M.hover_info(_, _, on_info)
on_info(
nil,
{
contents = {
kind = 'markdown',
contents = { 'No extra information availaible!' },
},
}
)
on_info(nil, {
contents = {
kind = 'markdown',
contents = { 'No extra information availaible!' },
},
})
end
---@param on_symbols function

View File

@@ -23,15 +23,12 @@ function M.hover_info(bufnr, params, on_info)
end
if not used_client then
on_info(
nil,
{
contents = {
kind = 'markdown',
content = { 'No extra information availaible!' },
},
}
)
on_info(nil, {
contents = {
kind = 'markdown',
content = { 'No extra information availaible!' },
},
})
end
used_client.request('textDocument/hover', params, on_info, bufnr)

View File

@@ -38,4 +38,76 @@ function M.debounce(f, delay)
end
end
function M.items_dfs(callback, children)
for _, val in ipairs(children) do
callback(val)
if val.children then
M.items_dfs(callback, val.children)
end
end
end
---Merges a symbol tree recursively, only replacing nodes
---which have changed. This will maintain the folding
---status of any unchanged nodes.
---@param new_node table New node
---@param old_node table Old node
---@param index? number Index of old_item in parent
---@param parent? table Parent of old_item
M.merge_items_rec = function(new_node, old_node, index, parent)
local failed = false
if not new_node or not old_node then
failed = true
else
for key, _ in pairs(new_node) do
if
vim.tbl_contains({
'parent',
'children',
'folded',
'hovered',
'line_in_outline',
'hierarchy',
}, key)
then
goto continue
end
if key == 'name' then
-- in the case of a rename, just rename the existing node
old_node['name'] = new_node['name']
else
if not vim.deep_equal(new_node[key], old_node[key]) then
failed = true
break
end
end
::continue::
end
end
if failed then
if parent and index then
parent[index] = new_node
end
else
local next_new_item = new_node.children or {}
-- in case new children are created on a node which
-- previously had no children
if #next_new_item > 0 and not old_node.children then
old_node.children = {}
end
local next_old_item = old_node.children or {}
for i = 1, math.max(#next_new_item, #next_old_item) do
M.merge_items_rec(next_new_item[i], next_old_item[i], i, next_old_item)
end
end
end
return M

View File

@@ -1,5 +1,6 @@
local parser = require 'symbols-outline.parser'
local config = require 'symbols-outline.config'
local ui = require 'symbols-outline.ui'
local M = {}
@@ -21,7 +22,7 @@ function M.write_outline(bufnr, lines)
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
end
function M.add_highlights(bufnr, hl_info)
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(
@@ -33,6 +34,8 @@ function M.add_highlights(bufnr, hl_info)
hl_end
)
end
M.add_hover_highlights(bufnr, nodes)
end
local ns = vim.api.nvim_create_namespace 'symbols-outline-virt-text'
@@ -58,6 +61,30 @@ local function clear_virt_text(bufnr)
vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1)
end
M.add_hover_highlights = function(bufnr, nodes)
if not config.options.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
local marker_fac = (config.options.fold_markers and 1) or 0
if node.prefix_length then
ui.add_hover_highlight(
bufnr,
node.line_in_outline - 1,
node.prefix_length
)
end
::continue::
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)
@@ -66,7 +93,7 @@ function M.parse_and_write(bufnr, flattened_outline_items)
clear_virt_text(bufnr)
local details = parser.get_details(flattened_outline_items)
M.add_highlights(bufnr, hl_info)
M.add_highlights(bufnr, hl_info, flattened_outline_items)
M.write_details(bufnr, details)
end