diff --git a/lua/outline/config.lua b/lua/outline/config.lua index c11cbdd..e1c6686 100644 --- a/lua/outline/config.lua +++ b/lua/outline/config.lua @@ -1,8 +1,7 @@ local utils = require('outline.utils') local M = {} - --- TODO: Types for config schema +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',} M.defaults = { guides = { @@ -77,7 +76,8 @@ M.defaults = { }, }, symbols = { - blacklist = {}, + ---@type outline.FilterConfig? + filter = nil, icon_source = nil, icon_fetcher = nil, icons = { @@ -166,6 +166,16 @@ function M.get_split_command() 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 @@ -176,11 +186,33 @@ local function has_value(tab, val) return false end -function M.is_symbol_blacklisted(kind) - if kind == nil then - return false +---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 - return has_value(M.o.symbols.blacklist, kind) + + 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 @@ -197,6 +229,8 @@ function M.is_client_blacklisted(client) 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 @@ -217,15 +251,26 @@ function M.show_help() print(vim.inspect(M.o.keymaps)) end ----Check for inconsistent or mutually exclusive opts. Does not alter the opts +---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", "hide_cursor enabled without cursorline enabled!") + utils.echo("config", "Warning: hide_cursor enabled without cursorline enabled") end end ----Resolve shortcuts and deprecated option conversions +---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 @@ -234,7 +279,7 @@ function M.resolve_config() 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 @@ -245,23 +290,113 @@ function M.resolve_config() M.o.keymaps.up_and_jump = ug M.o.keymaps.up_and_goto = nil end - -- if dg or ug then - -- vim.notify("[outline.config]: keymaps down/up_and_goto are renamed to down/up_and_jump. Your keymaps for the current session is converted successfully.", vim.log.levels.WARN) - -- 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 {}) - local guides = M.o.guides - if type(guides) == 'boolean' and guides then - M.o.guides = M.defaults.guides - end M.check_config() M.resolve_config() end diff --git a/lua/outline/init.lua b/lua/outline/init.lua index da72f69..84f8c45 100644 --- a/lua/outline/init.lua +++ b/lua/outline/init.lua @@ -78,11 +78,11 @@ local function __refresh() return end - local items = parser.parse(response) - _merge_items(items) - M.state.code_win = vim.api.nvim_get_current_win() + local items = parser.parse(response, vim.api.nvim_get_current_buf()) + _merge_items(items) + _update_lines() end @@ -445,7 +445,7 @@ local function handler(response, opts) setup_keymaps(M.view.bufnr) setup_buffer_autocmd() - local items = parser.parse(response) + local items = parser.parse(response, vim.api.nvim_win_get_buf(M.state.code_win)) M.state.outline_items = items M.state.flattened_outline_items = writer.make_outline(M.view.bufnr, items, M.state.code_win) @@ -457,11 +457,6 @@ local function handler(response, opts) end end ----@class outline.OutlineOpts ----@field focus_outline boolean? ----@field on_symbols function? ----@field on_outline_setup function? - ---Set position of outline window to match cursor position in code, return ---whether the window is just newly opened (previously not open). ---@param opts outline.OutlineOpts? Field `focus_outline` = `false` or `nil` means don't focus on outline window after following cursor. If opts is not provided, focus will be on outline window after following cursor. diff --git a/lua/outline/parser.lua b/lua/outline/parser.lua index c3dc490..b1b49a1 100644 --- a/lua/outline/parser.lua +++ b/lua/outline/parser.lua @@ -7,23 +7,6 @@ local folding = require 'outline.folding' local M = {} ----@class outline.SymbolNode ----@field name string ----@field depth integer ----@field parent outline.SymbolNode ----@field deprecated boolean ----@field kind integer|string ----@field icon string ----@field detail string ----@field line integer ----@field character integer ----@field range_start integer ----@field range_end integer ----@field isLast boolean ----@field hierarchy boolean ----@field children? outline.SymbolNode[] ----@field traversal_child integer - ---Parses result from LSP into a reorganized tree of symbols (not flattened, -- simply reoganized by merging each property table from the arguments into a -- table for each symbol) @@ -31,12 +14,14 @@ local M = {} ---@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 +---@param bufnr integer The buffer number which the result was from ---@return outline.SymbolNode[] -local function parse_result(result, depth, hierarchy, parent) +local function parse_result(result, depth, hierarchy, parent, bufnr) local ret = {} for index, value in pairs(result) do - if not cfg.is_symbol_blacklisted(symbols.kinds[value.kind]) then + -- FIXME: If a parent was excluded, all children will not be considered + if cfg.should_include_symbol(symbols.kinds[value.kind], bufnr) then -- the hierarchy is basically a table of booleans which -- tells whether the parent was the last in its group or -- not @@ -74,7 +59,7 @@ local function parse_result(result, depth, hierarchy, parent) -- 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) + children = parse_result(value.children, level + 1, child_hir, node, bufnr) else value.children = {} end @@ -89,11 +74,12 @@ end --'textDocument/documentSymbol', buf_request_all. ---Used when refreshing and setting up new symbols ---@param response table The result from buf_request_all +---@param bufnr integer ---@return outline.SymbolNode[] -function M.parse(response) +function M.parse(response, bufnr) local sorted = lsp_utils.sort_symbols(response) - return parse_result(sorted, nil, nil) + return parse_result(sorted, nil, nil, nil, bufnr) end ---Iterator that traverses the tree parent first before children, returning each node. diff --git a/lua/outline/types/outline.lua b/lua/outline/types/outline.lua new file mode 100644 index 0000000..abae9c8 --- /dev/null +++ b/lua/outline/types/outline.lua @@ -0,0 +1,68 @@ +-- CONFIG + +-- { 'String', 'Variable', exclude = true } +-- If FilterList is nil or false, means include all +---@class outline.FilterList: string[]? +---@field exclude boolean? If nil, means exclude=false, so the list would be an inclusive list. + +-- { +-- python = { 'Variable', exclude = true }, +-- go = { 'Field', 'Function', 'Method' }, +-- ['\*'] = { 'String', exclude = true } +-- } +---@alias outline.FilterFtList { [string]: outline.FilterList } A filter list for each file type +---@alias outline.FilterConfig outline.FilterFtList|outline.FilterList +-- { String = false, Variable = true, File = true, ... } +---@alias outline.FilterTable { [string]: boolean } Each kind:include pair where include is boolean, whether to include this kind. Used internally. +-- { +-- python = { String = false, Variable = true, ... }, +-- ['\*'] = { File = true, Method = true, ... }, +-- } +---@alias outline.FilterFtTable { [string]: outline.FilterTable } A filter table for each file type. Used internally. + +-- SYMBOLS + +---@class outline.SymbolNode +---@field name string +---@field depth integer +---@field parent outline.SymbolNode +---@field deprecated boolean +---@field kind integer|string +---@field icon string +---@field detail string +---@field line integer +---@field character integer +---@field range_start integer +---@field range_end integer +---@field isLast boolean +---@field hierarchy boolean +---@field children? outline.SymbolNode[] +---@field traversal_child integer Should NOT be modified during iteration using parser.preorder_iter + +---@class outline.FlatSymbolNode +---@field name string +---@field depth integer +---@field parent outline.SymbolNode +---@field deprecated boolean +---@field kind integer|string +---@field icon string +---@field detail string +---@field line integer +---@field character integer +---@field range_start integer +---@field range_end integer +---@field isLast boolean +---@field hierarchy boolean +---@field children? outline.SymbolNode[] +---@field traversal_child integer +---@field line_in_outline integer +---@field prefix_length integer +---@field hovered boolean +---@field folded boolean + +-- API + +---@class outline.OutlineOpts +---@field focus_outline boolean? Whether to focus on outline of after some operation. If nil, defaults to true +---@field on_symbols function? After symbols have been received, before sidebar window is setup +---@field on_outline_setup function? After sidebar window is setup diff --git a/lua/outline/utils/jsx.lua b/lua/outline/utils/jsx.lua index a341198..a8d9b1a 100644 --- a/lua/outline/utils/jsx.lua +++ b/lua/outline/utils/jsx.lua @@ -101,14 +101,14 @@ function M.parse_ts(root, children, bufnr) return children end -function M.get_symbols() +function M.get_symbols(bufnr) local status, parsers = pcall(require, 'nvim-treesitter.parsers') if not status then return {} end - local bufnr = 0 + bufnr = bufnr or 0 local parser = parsers.get_parser(bufnr) diff --git a/lua/outline/writer.lua b/lua/outline/writer.lua index a753bb4..99872af 100644 --- a/lua/outline/writer.lua +++ b/lua/outline/writer.lua @@ -73,27 +73,6 @@ function M.add_hover_highlights (bufnr, nodes) end end ----@class outline.FlatSymbolNode ----@field name string ----@field depth integer ----@field parent outline.SymbolNode ----@field deprecated boolean ----@field kind integer|string ----@field icon string ----@field detail string ----@field line integer ----@field character integer ----@field range_start integer ----@field range_end integer ----@field isLast boolean ----@field hierarchy boolean ----@field children? outline.SymbolNode[] ----@field traversal_child integer ----@field line_in_outline integer ----@field prefix_length integer ----@field hovered boolean ----@field folded boolean - ---The quintessential function of this entire plugin. Clears virtual text, -- parses each node and replaces old lines with new lines to be written for the -- outline buffer. diff --git a/scripts/convert-symbols-outline-opts.lua b/scripts/convert-symbols-outline-opts.lua index 7f33d2f..3ea2bd4 100644 --- a/scripts/convert-symbols-outline-opts.lua +++ b/scripts/convert-symbols-outline-opts.lua @@ -33,14 +33,15 @@ local your_new_opts = --[[put the cursor on this line]] { --------------------------------------------------------------------- ----- BEGIN SCRIPT -------------------------------------------------- --------------------------------------------------------------------- -local newopts = {} +local newopts = { symbols = {} } -if opts.symbols or opts.symbol_blacklist then - newopts.symbols = { - icons = opts.symbols, - blacklist = opts.symbol_blacklist, - } +if opts.symbols then + newopts.symbols.icons = opts.symbols opts.symbols = nil +end +if opts.symbol_blacklist then + newopts.symbols.filter = opts.symbol_blacklist + newopts.symbols.filter.exclude = true opts.symbol_blacklist = nil end @@ -109,7 +110,7 @@ if opts and next(opts) ~= nil then end end -for _, v in ipairs({'outline_items', 'outline_window', 'preview_window'}) do +for _, v in ipairs({'outline_items', 'outline_window', 'preview_window', 'symbols'}) do if newopts[v] and next(newopts[v]) == nil then newopts[v] = nil end