Files
outline.nvim/lua/outline/config.lua
hedy 9f69f12161 refactor: Reorganize modules
Primarily:
- Utils
- Sidebar (removed the need of writer.lua)
- Resolve keymaps shortcut in config eraly
- Put highlight functions into highlight.lua
- Put functions that do stuff on outline window into view.lua
2023-11-26 13:17:18 +08:00

452 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = {
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))
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 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 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
---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
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