Disabled by default, but if this feature turns out to be useful and stable, it will replace the old preview window.
461 lines
13 KiB
Lua
461 lines
13 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,
|
||
-- The two below are both for auto_update_events.follow
|
||
highlight_hovered_item = true,
|
||
-- On open, always followed. This is for auto_update_events.follow, whether
|
||
-- to auto update cursor position to reflect code location. If false, can
|
||
-- manually trigger with follow_cursor (API, command, keymap action).
|
||
auto_set_cursor = true,
|
||
auto_update_events = {
|
||
follow = { 'CursorMoved' },
|
||
items = { 'InsertLeave', 'WinEnter', 'BufEnter', 'BufWinEnter', 'BufWritePost' },
|
||
},
|
||
},
|
||
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 = '',
|
||
jump_highlight_duration = 400,
|
||
center_on_jump = true,
|
||
},
|
||
preview_window = {
|
||
live = false,
|
||
auto_preview = false,
|
||
width = 50,
|
||
min_width = 50,
|
||
relative_width = true,
|
||
min_height = 10,
|
||
border = 'single',
|
||
open_hover_on_preview = false,
|
||
winhl = 'NormalFloat:',
|
||
winblend = 0,
|
||
},
|
||
symbol_folding = {
|
||
autofold_depth = nil,
|
||
auto_unfold = {
|
||
hovered = true,
|
||
---@type boolean|integer
|
||
only = 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', 'norg' },
|
||
lsp = {
|
||
blacklist_clients = {},
|
||
},
|
||
},
|
||
symbols = {
|
||
---@type outline.FilterConfig?
|
||
filter = nil,
|
||
icon_source = nil,
|
||
icon_fetcher = nil,
|
||
icons = {
|
||
File = { icon = '', hl = 'Identifier' },
|
||
Module = { icon = '', hl = 'Include' },
|
||
Namespace = { icon = '', hl = 'Include' },
|
||
Package = { icon = '', hl = 'Include' },
|
||
Class = { icon = '𝓒', hl = 'Type' },
|
||
Method = { icon = 'ƒ', hl = 'Function' },
|
||
Property = { icon = '', hl = 'Identifier' },
|
||
Field = { icon = '', hl = 'Identifier' },
|
||
Constructor = { icon = '', hl = 'Special' },
|
||
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 = 'Identifier' },
|
||
Struct = { icon = '𝓢', hl = 'Structure' },
|
||
Event = { icon = '🗲', hl = 'Type' },
|
||
Operator = { icon = '+', hl = 'Identifier' },
|
||
TypeParameter = { icon = '𝙏', hl = 'Identifier' },
|
||
Component = { icon = '', hl = 'Function' },
|
||
Fragment = { icon = '', hl = 'Constant' },
|
||
-- ccls
|
||
TypeAlias = { icon = ' ', hl = 'Type' },
|
||
Parameter = { icon = ' ', hl = 'Identifier' },
|
||
StaticMethod = { icon = ' ', hl = 'Function' },
|
||
Macro = { icon = ' ', hl = 'Function' },
|
||
},
|
||
},
|
||
}
|
||
|
||
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))
|
||
end
|
||
return M.o.outline_window.width
|
||
end
|
||
|
||
function M.get_preview_width()
|
||
if M.o.preview_window.relative_width then
|
||
local relative_width = math.max(
|
||
math.ceil(vim.o.columns * (M.o.preview_window.width / 100)),
|
||
M.o.preview_window.min_width
|
||
)
|
||
|
||
return relative_width
|
||
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 and norg as of now
|
||
if ft == 'markdown' or ft == 'norg' or kind == nil then
|
||
return true
|
||
end
|
||
|
||
local filter_table = M.o.symbols.filter[ft]
|
||
local default_filter_table = M.o.symbols.filter.default
|
||
|
||
-- 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 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 M.o.providers.lsp.blacklist_clients[client.name] == true
|
||
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
|
||
|
||
---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
|
||
if not M.o.guides.enabled then
|
||
M.o.guides = {
|
||
enabled = true,
|
||
markers = { middle = ' ', vertical = ' ', bottom = ' ' },
|
||
}
|
||
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()
|
||
----- AUTO UNFOLD -----
|
||
local au = M.o.symbol_folding.auto_unfold
|
||
if M.o.symbol_folding.auto_unfold_hover == nil then
|
||
if au.hovered ~= nil then
|
||
M.o.symbol_folding.auto_unfold_hover = au.hovered
|
||
end
|
||
end
|
||
if type(au.only) ~= 'number' then
|
||
au.only = (au.only and 1) or 0
|
||
end
|
||
----- KEYMAPS -----
|
||
for action, keys in pairs(M.o.keymaps) do
|
||
if type(keys) == 'string' then
|
||
M.o.keymaps[action] = { keys }
|
||
end
|
||
end
|
||
----- PREVIEW -----
|
||
M.o.preview_window.width = M.get_preview_width()
|
||
----- WINDOW -----
|
||
M.o.outline_window.width = M.get_window_width()
|
||
----- LSP BLACKLIST -----
|
||
local map = {}
|
||
for _, client in ipairs(M.o.providers.lsp.blacklist_clients) do
|
||
map[client] = true
|
||
end
|
||
M.o.providers.lsp.blacklist_clients = map
|
||
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 = { default = M.o.symbols.blacklist }
|
||
tmp.default.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 = { default = { exclude = true } }
|
||
|
||
-- Lazy filter list -> ft filter list
|
||
elseif tmp[1] then
|
||
if type(tmp[1]) == 'string' then
|
||
tmp = { default = vim.deepcopy(tmp) }
|
||
else
|
||
tmp.default = vim.deepcopy(tmp[1])
|
||
tmp[1] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
M.o.symbols.user_config_filter = vim.deepcopy(tmp)
|
||
|
||
---@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, ... },
|
||
-- default = { 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
|