Files
outline.nvim/lua/outline/config.lua
hedy d35ee70f95 feat: Better highlight-hover/follow-cursor procedures
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 :)
2023-11-18 09:34:16 +08:00

423 lines
12 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,
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