Previously on each outline open, the `writer.make_outline` function might be called at least 4 times(!), after this refactor it will only be called once. And on update cursor autocmds, also called once (previously at least twice). behaviour: - Now the outline window focus and highlight can update on each cursor move (previously CursorHold, dependent on updatetime). This is now configurable as well. - During fold-all/unfold-all operations, now the cursor will remain on the same node (rather than same line in outline buffer). - The performance improvement is not significantly observable since even the old implementation can appear instant. One may even argue I am fixing a problem that did not exist, but implementation-wise it's just so much better now. config: - outline_window.auto_update_events, list of events to be passed to create_user_autocmd for updating cursor focus in outline, and updating outline items (refetching symbols), using keys cursor and items respectively. - outline_window.show_cursorline now supports 2 other string values: 'focus_in_outline'/'focus_in_code' which controls when to enable cursorline. Setting to true retains the default behaviour of always showing the cursorline. This was added because now that the cursor focus on the outline could change on each CursorMoved, the cursorline may pose to be qute attention-seeking during the outline cursor updates. Hence `focus_in_outline` is added so that when focus is in code, the cursorline for outline window is not shown. 'focus_in_code' is added so that a user who disabled highlight_hovered_item can keep track of position in outline when focus is in code, disabling cursorline when focus is in outline. At any given time, if hide cursor is enabled and show_cursorline is a string value, hiding of cursor will not be done if cursorline is not shown in the the given situation. implementation: - The reason for the improvement in performance as described in the first paragraph is due to merging of finding hover item and finding the deepest matched node to put cursor, into writer.make_outline. This done, when previously done in separate function, because after the separate function (namely _highlight_hovered_item) finishes, writer.make_outline is called *again* anyway. - Autocmds to update cursor position in outline is now done per buffer rather than global. Somehow the auto unfold and unfold depth options still work perfectly, for this we should thank simrat or which ever contributor that modularized the folding module and made it adaptable :)
423 lines
12 KiB
Lua
423 lines
12 KiB
Lua
local utils = require('outline.utils')
|
||
|
||
local M = {}
|
||
-- stylua: ignore start
|
||
local all_kinds = {'File', 'Module', 'Namespace', 'Package', 'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum', 'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct', 'Event', 'Operator', 'TypeParameter', 'Component', 'Fragment', 'TypeAlias', 'Parameter', 'StaticMethod', 'Macro'}
|
||
-- stylua: ignore end
|
||
|
||
M.defaults = {
|
||
guides = {
|
||
enabled = true,
|
||
markers = {
|
||
bottom = '└',
|
||
middle = '├',
|
||
vertical = '│',
|
||
horizontal = '─',
|
||
},
|
||
},
|
||
outline_items = {
|
||
show_symbol_details = true,
|
||
show_symbol_lineno = false,
|
||
highlight_hovered_item = true,
|
||
},
|
||
outline_window = {
|
||
position = 'right',
|
||
split_command = nil,
|
||
width = 25,
|
||
relative_width = true,
|
||
wrap = false,
|
||
focus_on_open = true,
|
||
auto_close = false,
|
||
auto_jump = false,
|
||
show_numbers = false,
|
||
show_relative_numbers = false,
|
||
---@type boolean?|string?
|
||
show_cursorline = true,
|
||
hide_cursor = false,
|
||
winhl = 'OutlineDetails:Comment,OutlineLineno:LineNr',
|
||
jump_highlight_duration = 400,
|
||
center_on_jump = true,
|
||
auto_update_events = {
|
||
cursor = { 'CursorMoved' },
|
||
items = { 'InsertLeave', 'WinEnter', 'BufEnter', 'BufWinEnter', 'TabEnter', 'BufWritePost' },
|
||
},
|
||
},
|
||
preview_window = {
|
||
auto_preview = false,
|
||
width = 50,
|
||
min_width = 50,
|
||
relative_width = true,
|
||
border = 'single',
|
||
open_hover_on_preview = false,
|
||
winhl = '',
|
||
winblend = 0,
|
||
},
|
||
symbol_folding = {
|
||
autofold_depth = nil,
|
||
auto_unfold_hover = true,
|
||
markers = { '', '' },
|
||
},
|
||
keymaps = {
|
||
show_help = '?',
|
||
close = { '<Esc>', 'q' },
|
||
goto_location = '<Cr>',
|
||
peek_location = 'o',
|
||
goto_and_close = '<S-Cr>',
|
||
restore_location = '<C-g>',
|
||
hover_symbol = '<C-space>',
|
||
toggle_preview = 'K',
|
||
rename_symbol = 'r',
|
||
code_actions = 'a',
|
||
fold = 'h',
|
||
fold_toggle = '<tab>',
|
||
fold_toggle_all = '<S-tab>',
|
||
unfold = 'l',
|
||
fold_all = 'W',
|
||
unfold_all = 'E',
|
||
fold_reset = 'R',
|
||
down_and_jump = '<C-j>',
|
||
up_and_jump = '<C-k>',
|
||
},
|
||
providers = {
|
||
priority = { 'lsp', 'coc', 'markdown' },
|
||
lsp = {
|
||
blacklist_clients = {},
|
||
},
|
||
},
|
||
symbols = {
|
||
---@type outline.FilterConfig?
|
||
filter = nil,
|
||
icon_source = nil,
|
||
icon_fetcher = nil,
|
||
icons = {
|
||
File = { icon = '', hl = '@text.uri' },
|
||
Module = { icon = '', hl = '@namespace' },
|
||
Namespace = { icon = '', hl = '@namespace' },
|
||
Package = { icon = '', hl = '@namespace' },
|
||
Class = { icon = '𝓒', hl = '@type' },
|
||
Method = { icon = 'ƒ', hl = '@method' },
|
||
Property = { icon = '', hl = '@method' },
|
||
Field = { icon = '', hl = '@field' },
|
||
Constructor = { icon = '', hl = '@constructor' },
|
||
Enum = { icon = 'ℰ', hl = '@type' },
|
||
Interface = { icon = '', hl = '@type' },
|
||
Function = { icon = '', hl = '@function' },
|
||
Variable = { icon = '', hl = '@constant' },
|
||
Constant = { icon = '', hl = '@constant' },
|
||
String = { icon = '𝓐', hl = '@string' },
|
||
Number = { icon = '#', hl = '@number' },
|
||
Boolean = { icon = '⊨', hl = '@boolean' },
|
||
Array = { icon = '', hl = '@constant' },
|
||
Object = { icon = '⦿', hl = '@type' },
|
||
Key = { icon = '🔐', hl = '@type' },
|
||
Null = { icon = 'NULL', hl = '@type' },
|
||
EnumMember = { icon = '', hl = '@field' },
|
||
Struct = { icon = '𝓢', hl = '@type' },
|
||
Event = { icon = '🗲', hl = '@type' },
|
||
Operator = { icon = '+', hl = '@operator' },
|
||
TypeParameter = { icon = '𝙏', hl = '@parameter' },
|
||
Component = { icon = '', hl = '@function' },
|
||
Fragment = { icon = '', hl = '@constant' },
|
||
-- ccls
|
||
TypeAlias = { icon = ' ', hl = '@type' },
|
||
Parameter = { icon = ' ', hl = '@parameter' },
|
||
StaticMethod = { icon = ' ', hl = '@function' },
|
||
Macro = { icon = ' ', hl = '@macro' },
|
||
},
|
||
},
|
||
}
|
||
|
||
M.o = {}
|
||
|
||
function M.has_numbers()
|
||
return M.o.outline_window.show_numbers or M.o.outline_window.show_relative_numbers
|
||
end
|
||
|
||
function M.get_position_navigation_direction()
|
||
if M.o.outline_window.position == 'left' then
|
||
return 'h'
|
||
else
|
||
return 'l'
|
||
end
|
||
end
|
||
|
||
function M.get_window_width()
|
||
if M.o.outline_window.relative_width then
|
||
return math.ceil(vim.o.columns * (M.o.outline_window.width / 100))
|
||
else
|
||
return M.o.outline_window.width
|
||
end
|
||
end
|
||
|
||
function M.get_preview_width()
|
||
if M.o.preview_window.relative_width then
|
||
local relative_width = math.ceil(vim.o.columns * (M.o.preview_window.width / 100))
|
||
|
||
if relative_width < M.o.preview_window.min_width then
|
||
return M.o.preview_window.min_width
|
||
else
|
||
return relative_width
|
||
end
|
||
else
|
||
return M.o.preview_window.width
|
||
end
|
||
end
|
||
|
||
function M.get_split_command()
|
||
local sc = M.o.outline_window.split_command
|
||
if sc then
|
||
return sc
|
||
end
|
||
if M.o.outline_window.position == 'left' then
|
||
return 'topleft vs'
|
||
else
|
||
return 'botright vs'
|
||
end
|
||
end
|
||
|
||
---Whether table == {}
|
||
---@param t table
|
||
local function is_empty_table(t)
|
||
return t and next(t) == nil
|
||
end
|
||
|
||
local function table_has_content(t)
|
||
return t and next(t) ~= nil
|
||
end
|
||
|
||
local function has_value(tab, val)
|
||
for _, value in ipairs(tab) do
|
||
if value == val then
|
||
return true
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
---Determine whether to include symbol in outline based on bufnr and its kind
|
||
---@param kind string
|
||
---@param bufnr integer
|
||
---@return boolean include
|
||
function M.should_include_symbol(kind, bufnr)
|
||
local ft = vim.api.nvim_buf_get_option(bufnr, 'ft')
|
||
-- There can only be one kind in markdown as of now
|
||
if ft == 'markdown' or kind == nil then
|
||
return true
|
||
end
|
||
|
||
local filter_table = M.o.symbols.filter[ft]
|
||
local default_filter_table = M.o.symbols.filter['*']
|
||
|
||
-- When filter table for a ft is not specified, all symbols are shown
|
||
if not filter_table then
|
||
if not default_filter_table then
|
||
return true
|
||
else
|
||
return default_filter_table[kind] ~= false
|
||
end
|
||
end
|
||
|
||
-- XXX: If the given kind is not known by outline.nvim (ie: not in
|
||
-- all_kinds), still return true. Only exclude those symbols that were
|
||
-- explicitly filtered out.
|
||
return filter_table[kind] ~= false
|
||
end
|
||
|
||
---@param client vim.lsp.client|number
|
||
function M.is_client_blacklisted(client)
|
||
if not client then
|
||
return false
|
||
end
|
||
if type(client) == 'number' then
|
||
client = vim.lsp.get_client_by_id(client)
|
||
if not client then
|
||
return false
|
||
end
|
||
end
|
||
return has_value(M.o.providers.lsp.blacklist_clients, client.name)
|
||
end
|
||
|
||
---Retrieve and cache import paths of all providers in order of given priority
|
||
---@return string[]
|
||
function M.get_providers()
|
||
if M.providers then
|
||
return M.providers
|
||
end
|
||
|
||
M.providers = {}
|
||
for _, p in ipairs(M.o.providers.priority) do
|
||
if p == 'lsp' then
|
||
p = 'nvim-lsp' -- due to legacy reasons
|
||
end
|
||
table.insert(M.providers, p)
|
||
end
|
||
return M.providers
|
||
end
|
||
|
||
function M.show_help()
|
||
print('Current keymaps:')
|
||
print(vim.inspect(M.o.keymaps))
|
||
end
|
||
|
||
---Check for inconsistent or mutually exclusive opts.
|
||
-- Does not alter the opts. Might show messages.
|
||
function M.check_config()
|
||
if M.o.outline_window.hide_cursor and not M.o.outline_window.show_cursorline then
|
||
utils.echo('config', 'Warning: hide_cursor enabled without cursorline enabled')
|
||
end
|
||
end
|
||
|
||
---Resolve shortcuts and deprecated option conversions.
|
||
-- Might alter opts. Might show messages.
|
||
function M.resolve_config()
|
||
----- GUIDES -----
|
||
local guides = M.o.guides
|
||
if type(guides) == 'boolean' then
|
||
M.o.guides = M.defaults.guides
|
||
if not guides then
|
||
M.o.guides.enabled = false
|
||
end
|
||
end
|
||
----- SPLIT COMMAND -----
|
||
local sc = M.o.outline_window.split_command
|
||
if sc then
|
||
-- This should not be needed, nor is it failsafe. But in case user only provides
|
||
-- the, eg, "topleft", we append the ' vs'.
|
||
if not sc:find(' vs', 1, true) then
|
||
M.o.outline_window.split_command = sc .. ' vs'
|
||
end
|
||
end
|
||
----- COMPAT (renaming) -----
|
||
local dg = M.o.keymaps.down_and_goto
|
||
local ug = M.o.keymaps.up_and_goto
|
||
if dg then
|
||
M.o.keymaps.down_and_jump = dg
|
||
M.o.keymaps.down_and_goto = nil
|
||
end
|
||
if ug then
|
||
M.o.keymaps.up_and_jump = ug
|
||
M.o.keymaps.up_and_goto = nil
|
||
end
|
||
if M.o.outline_window.auto_goto then
|
||
M.o.outline_window.auto_jump = M.o.outline_window.auto_goto
|
||
M.o.outline_window.auto_goto = nil
|
||
end
|
||
----- SYMBOLS FILTER -----
|
||
M.resolve_filter_config()
|
||
end
|
||
|
||
---Ensure l is either table, false, or nil. If not, print warning using given
|
||
-- name that describes l, set l to nil, and return l.
|
||
---@generic T
|
||
---@param l T
|
||
---@param name string
|
||
---@return T
|
||
local function validate_filter_list(l, name)
|
||
if type(l) == 'boolean' and l then
|
||
utils.echo(
|
||
'config',
|
||
('Setting %s to true is undefined behaviour. Defaulting to nil.'):format(name)
|
||
)
|
||
l = nil
|
||
elseif l and type(l) ~= 'table' and type(l) ~= 'boolean' then
|
||
utils.echo(
|
||
'config',
|
||
('%s must either be a table, false, or nil. Defaulting to nil.'):format(name)
|
||
)
|
||
l = nil
|
||
end
|
||
return l
|
||
end
|
||
|
||
---Resolve shortcuts and compat opt for symbol filtering config, and set up
|
||
-- `M.o.symbols.filter` to be a proper `outline.FilterFtTable` lookup table.
|
||
function M.resolve_filter_config()
|
||
---@type outline.FilterConfig
|
||
local tmp = M.o.symbols.filter
|
||
tmp = validate_filter_list(tmp, 'symbols.filter')
|
||
|
||
---- legacy form -> ft filter list ----
|
||
if table_has_content(M.o.symbols.blacklist) then
|
||
tmp = { ['*'] = M.o.symbols.blacklist }
|
||
tmp['*'].exclude = true
|
||
M.o.symbols.blacklist = nil
|
||
else
|
||
---- nil or {} -> include all symbols ----
|
||
-- For filter = {}: theoretically this would make no symbols show up. The
|
||
-- user can't possibly want this (they should've disabled the plugin
|
||
-- through the plugin manager); so we let filter = {} denote filter = nil
|
||
-- (or false), meaning include all symbols.
|
||
if not table_has_content(tmp) then
|
||
tmp = { ['*'] = { exclude = true } }
|
||
|
||
-- Lazy filter list -> ft filter list
|
||
elseif tmp[1] then
|
||
if type(tmp[1]) == 'string' then
|
||
tmp = { ['*'] = vim.deepcopy(tmp) }
|
||
else
|
||
tmp['*'] = vim.deepcopy(tmp[1])
|
||
tmp[1] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
---@type outline.FilterFtList
|
||
local filter = tmp
|
||
---@type outline.FilterFtTable
|
||
M.o.symbols.filter = {}
|
||
|
||
---- ft filter list -> lookup table ----
|
||
-- We do this so that all the O(N) checks happen once, in the setup phase,
|
||
-- and checks for the filter list later on can be speedy.
|
||
-- After this operation, filter table would have ft as keys, and for each
|
||
-- value, it has each kind key denoting whether to include that kind for this
|
||
-- filetype.
|
||
-- {
|
||
-- python = { String = false, Variable = true, ... },
|
||
-- ['*'] = { File = true, Method = true, ... },
|
||
-- }
|
||
for ft, list in pairs(filter) do
|
||
if type(ft) ~= 'string' then
|
||
utils.echo(
|
||
'config',
|
||
'ft (keys) for symbols.filter table can only be string. Skipping this ft.'
|
||
)
|
||
goto continue
|
||
end
|
||
|
||
M.o.symbols.filter[ft] = {}
|
||
|
||
list = validate_filter_list(list, ("filter list for ft '%s'"):format(ft))
|
||
|
||
-- Ensure boolean.
|
||
-- Catches setting some ft = false/nil, meaning include all kinds
|
||
if not list then
|
||
list = { exclude = true }
|
||
else
|
||
list.exclude = (list.exclude ~= nil and list.exclude) or false
|
||
end
|
||
|
||
-- If it's an exclude-list, set all kinds to be included (true) by default
|
||
-- If it's an inclusive list, set all kinds to be excluded (false) by default
|
||
for _, kind in pairs(all_kinds) do
|
||
M.o.symbols.filter[ft][kind] = list.exclude
|
||
end
|
||
|
||
-- Now flip the switches
|
||
for _, kind in ipairs(list) do
|
||
M.o.symbols.filter[ft][kind] = not M.o.symbols.filter[ft][kind]
|
||
end
|
||
::continue::
|
||
end
|
||
end
|
||
|
||
function M.setup(options)
|
||
vim.g.outline_loaded = 1
|
||
M.o = vim.tbl_deep_extend('force', {}, M.defaults, options or {})
|
||
M.check_config()
|
||
M.resolve_config()
|
||
end
|
||
|
||
return M
|