diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ebddc..b283cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,19 +36,21 @@ `CursorHold`. This is also configurable now using `outline_items.auto_update_events` with key `follow`. The key `items` controls the events that should trigger a re-request of symbols from the provider. -- Config option for cursorline now supports 2 other string values, - `focus_in_outline` and `focus_in_code`. These make the cursorline only show up - depending on cursor focus. The first option, `focus_in_outline` makes it so - cursorline is enabled only when focus is in outline. This lessens the visual - changes due to auto follow_cursor. - On fold all or unfold all operations, the cursor will now stay on the same node, rather than the same line in the outline. - Optionally not automatically update cursor position in outline to reflect cursor location in code. The auto-update is triggered by events from - `outline_items.auto_update_events.follow`, and disabling of follow-cursor can - be done through `outline_items.auto_follow_cursor`. Regardless, manual - follow-cursor can still be done using `:FollowCursor[!]` or lua API - `follow_cursor({ focus_outline = true/false })`. + `outline_items.auto_update_events.follow` which controls both highlighting of + hovered node and also setting of cursor in outline. To disable the latter, + use `outline_items.auto_set_cursor`. Disabling the former can still be done using + `outline_items.highlight_hovered_item`. Regardless, manual follow-cursor can + still be done using `:FollowCursor[!]` or lua API `follow_cursor({ + focus_outline = true/false })`. +- Config option for cursorline now supports 2 other string values, + `focus_in_outline` and `focus_in_code`. These make the cursorline only show up + depending on cursor focus. The first option, `focus_in_outline` makes it so + cursorline is enabled only when focus is in outline. This lessens the visual + changes due to `auto_set_cursor`, when focus is in code. ### Fixes diff --git a/lua/outline/config.lua b/lua/outline/config.lua index 577caf1..1bedce8 100644 --- a/lua/outline/config.lua +++ b/lua/outline/config.lua @@ -23,7 +23,7 @@ M.defaults = { -- 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_follow_cursor = true, + auto_set_cursor = true, auto_update_events = { follow = { 'CursorMoved' }, items = { 'InsertLeave', 'WinEnter', 'BufEnter', 'BufWinEnter', 'TabEnter', 'BufWritePost' }, @@ -40,7 +40,7 @@ M.defaults = { auto_jump = false, show_numbers = false, show_relative_numbers = false, - ---@type boolean?|string? + ---@type boolean|string? show_cursorline = true, hide_cursor = false, winhl = 'OutlineDetails:Comment,OutlineLineno:LineNr', @@ -52,6 +52,7 @@ M.defaults = { width = 50, min_width = 50, relative_width = true, + min_height = 10, border = 'single', open_hover_on_preview = false, winhl = '', diff --git a/lua/outline/init.lua b/lua/outline/init.lua index a042809..3f39bda 100644 --- a/lua/outline/init.lua +++ b/lua/outline/init.lua @@ -50,6 +50,9 @@ local function wipe_state() } end +---Calls writer.make_outline and then calls M.update_cursor_pos if update_cursor is not false +---@param update_cursor boolean? +---@param set_cursor_to_node outline.SymbolNode|outline.FlatSymbolNode? local function _update_lines(update_cursor, set_cursor_to_node) local current M.state.flattened_outline_items, current = @@ -64,6 +67,35 @@ local function _merge_items(items) utils.merge_items_rec({ children = items }, { children = M.state.outline_items }) end +---Setup autocmds for the buffer that the outline attached to +---@param code_win integer Must be valid +---@param code_buf integer Must be valid +local function setup_attached_buffer_autocmd(code_win, code_buf) + local events = cfg.o.outline_items.auto_update_events + if + cfg.o.outline_items.highlight_hovered_item + or cfg.o.symbol_folding.auto_unfold_hover + then + if M.state.autocmds[code_win] then + vim.api.nvim_del_autocmd(M.state.autocmds[code_win]) + M.state.autocmds[code_win] = nil + end + + if utils.str_or_nonempty_table(events.follow) then + M.state.autocmds[code_win] = + vim.api.nvim_create_autocmd(events.follow, { + buffer = code_buf, + callback = function() + M._highlight_current_item( + code_win, + cfg.o.outline_items.auto_set_cursor + ) + end, + }) + end + end +end + local function __refresh() local current_buffer_is_outline = M.view.bufnr == vim.api.nvim_get_current_buf() if M.view:is_open() and not current_buffer_is_outline then @@ -82,29 +114,16 @@ local function __refresh() M.state.autocmds[M.state.code_win] = nil end end + M.state.code_win = curwin M.state.code_buf = curbuf - if cfg.o.outline_items.highlight_hovered_item or cfg.o.symbol_folding.auto_unfold_hover then - if M.state.autocmds[curwin] then - vim.api.nvim_del_autocmd(M.state.autocmds[curwin]) - M.state.autocmds[curwin] = nil - end - if utils.str_or_nonempty_table(cfg.o.outline_items.auto_update_events.follow) then - M.state.autocmds[curwin] = - vim.api.nvim_create_autocmd(cfg.o.outline_items.auto_update_events.follow, { - buffer = curbuf, - callback = function() - M._highlight_current_item(nil, cfg.o.outline_items.auto_follow_cursor) - end, - }) - end - end + setup_attached_buffer_autocmd(curwin, curbuf) local items = parser.parse(response, vim.api.nvim_get_current_buf()) _merge_items(items) - local update_cursor = newbuf or cfg.o.outline_items.auto_follow_cursor + local update_cursor = newbuf or cfg.o.outline_items.auto_set_cursor _update_lines(update_cursor) end @@ -326,13 +345,22 @@ function M._highlight_current_item(winnr, update_cursor) return end - -- TODO: Find an efficient way to: - -- 1) Set highlight for all nodes in range (regardless of visibility) - -- 2) Find the line number of the deepest node in range, that is visible (no - -- parents folded) - -- In one go + local valid_code_win = vim.api.nvim_win_is_valid(M.state.code_win) + local valid_winnr = winnr and vim.api.nvim_win_is_valid(winnr) - -- XXX: Could current win ~= M.state.code_win here? + if not valid_code_win then + -- Definetely don't attempt to update anything if code win is no longer valid + return + end + + if not valid_winnr then + return + elseif winnr ~= M.state.code_win then + -- Both valid, but given winnr ~= known code_win. + -- Best not to handle this situation at all to prevent any unwanted side + -- effects + return + end _update_lines(update_cursor) end @@ -421,35 +449,16 @@ local function handler(response, opts) return end + if not opts then + opts = {} + end + M.state.code_win = vim.api.nvim_get_current_win() M.state.code_buf = vim.api.nvim_get_current_buf() M.state.opened_first_outline = true - if opts and opts.on_symbols then - opts.on_symbols() - end - - M.view:setup_view() - - if opts and opts.on_outline_setup then - opts.on_outline_setup() - end - - if cfg.o.outline_items.highlight_hovered_item or cfg.o.symbol_folding.auto_unfold_hover then - if M.state.autocmds[M.state.code_win] then - vim.api.nvim_del_autocmd(M.state.autocmds[M.state.code_win]) - M.state.autocmds[M.state.code_win] = nil - end - if utils.str_or_nonempty_table(cfg.o.outline_items.auto_update_events.follow) then - M.state.autocmds[M.state.code_win] = - vim.api.nvim_create_autocmd(cfg.o.outline_items.auto_update_events.follow, { - buffer = M.state.code_buf, - callback = function() - M._highlight_current_item(nil, cfg.o.outline_items.auto_follow_cursor) - end, - }) - end - end + local sc = opts.split_command or cfg.get_split_command() + M.view:setup_view(sc) -- clear state when buffer is closed vim.api.nvim_buf_attach(M.view.bufnr, false, { @@ -460,6 +469,7 @@ local function handler(response, opts) setup_keymaps(M.view.bufnr) setup_buffer_autocmd() + setup_attached_buffer_autocmd(M.state.code_win, M.state.code_buf) local items = parser.parse(response, M.state.code_buf) @@ -470,7 +480,7 @@ local function handler(response, opts) M.update_cursor_pos(current) - if not cfg.o.outline_window.focus_on_open or (opts and not opts.focus_outline) then + if not cfg.o.outline_window.focus_on_open or not opts.focus_outline then vim.fn.win_gotoid(M.state.code_win) end end @@ -531,26 +541,13 @@ end local function _cmd_open_with_mods(fn) return function(opts) - local old_sc, use_old_sc - local split = opts.smods.split - if split ~= '' then - old_sc = cfg.o.outline_window.split_command - use_old_sc = true - cfg.o.outline_window.split_command = split .. ' vsplit' + local fnopts = { focus_outline = not opts.bang } + local sc = opts.smods.split + if sc ~= '' then + fnopts.split_command = sc .. ' vsplit' end - local function on_outline_setup() - if use_old_sc then - cfg.o.outline_window.split_command = old_sc - -- the old option should already have been resolved during set up - end - end - - if opts.bang then - fn({ focus_outline = false, on_outline_setup = on_outline_setup }) - else - fn({ focus_outline = true, on_outline_setup = on_outline_setup }) - end + fn(fnopts) end end diff --git a/lua/outline/preview.lua b/lua/outline/preview.lua index 056be99..e7e77bd 100644 --- a/lua/outline/preview.lua +++ b/lua/outline/preview.lua @@ -26,29 +26,37 @@ end M.has_code_win = has_code_win -local function get_width_offset() +---Get the correct column to place the floating window based on +-- Relative positions of the outline and the code window. +---@param preview_width integer +local function get_col(preview_width) ---@type integer local outline_winnr = outline.view.winnr - local width = cfg.get_preview_width() + 3 - local has_numbers = vim.api.nvim_win_get_option(outline_winnr, 'number') - has_numbers = has_numbers or vim.api.nvim_win_get_option(outline_winnr, 'relativenumber') + local outline_col = vim.api.nvim_win_get_position(outline_winnr)[2] + local outline_width = vim.api.nvim_win_get_width(outline_winnr) + local code_col = vim.api.nvim_win_get_position(outline.state.code_win)[2] - if has_numbers then - width = width + 4 - end + -- TODO: What if code win is below/above outline instead? - -- FIXME: use actual window position based on view rather than config - if cfg.o.outline_window.position == 'right' then - width = 0 - width + local col = outline_col + if outline_col > code_col then + col = col - preview_width - 3 else - width = vim.api.nvim_win_get_width(outline_winnr) + 1 + col = col + outline_width + 1 end - return width + return col +end + +---@param preview_height integer +---@param outline_height integer +local function get_row(preview_height, outline_height) + local offset = math.floor((outline_height - preview_height) / 2) - 1 + return vim.api.nvim_win_get_position(outline.view.winnr)[1] + offset end local function get_height() - return vim.api.nvim_list_uis()[1].height + return vim.api.nvim_win_get_height(outline.view.winnr) end local function get_hovered_node() @@ -106,15 +114,16 @@ local function show_preview() end, }) local height = get_height() - local winheight = math.ceil(height / 2) + local width = cfg.get_preview_width() + local winheight = math.max(math.ceil(height / 2), cfg.o.preview_window.min_height) state.preview_win = vim.api.nvim_open_win(state.preview_buf, false, { - relative = 'win', + relative = 'editor', height = winheight, - width = cfg.get_preview_width(), + width = width, bufpos = { 0, 0 }, - col = get_width_offset(), + col = get_col(width), -- Position preview window middle-aligned vertically - row = math.floor((height - winheight) / 2) - 1, + row = get_row(winheight, height), border = cfg.o.preview_window.border, }) setup_preview_buf() diff --git a/lua/outline/types/outline.lua b/lua/outline/types/outline.lua index d7aa6bb..f521881 100644 --- a/lua/outline/types/outline.lua +++ b/lua/outline/types/outline.lua @@ -42,7 +42,7 @@ ---@class outline.FlatSymbolNode ---@field name string ---@field depth integer ----@field parent outline.SymbolNode +---@field parent outline.FlatSymbolNode ---@field deprecated boolean ---@field kind integer|string ---@field icon string @@ -53,7 +53,7 @@ ---@field range_end integer ---@field isLast boolean ---@field hierarchy boolean ----@field children? outline.SymbolNode[] +---@field children? outline.FlatSymbolNode[] ---@field traversal_child integer ---@field line_in_outline integer ---@field prefix_length integer @@ -64,5 +64,4 @@ ---@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 +---@field split_command string? diff --git a/lua/outline/view.lua b/lua/outline/view.lua index 8e4de68..4093e0a 100644 --- a/lua/outline/view.lua +++ b/lua/outline/view.lua @@ -6,15 +6,17 @@ function View:new() return setmetatable({ bufnr = nil, winnr = nil }, { __index = View }) end ----creates the outline window and sets it up -function View:setup_view() +---Creates the outline window and sets it up +---@param split_command string A valid split command that is to be executed in order to create the view. +function View:setup_view(split_command) -- create a scratch unlisted buffer self.bufnr = vim.api.nvim_create_buf(false, true) -- delete buffer when window is closed / buffer is hidden vim.api.nvim_buf_set_option(self.bufnr, 'bufhidden', 'delete') -- create a split - vim.cmd(cfg.get_split_command()) + vim.cmd(split_command) + -- resize to a % of the current window size vim.cmd('vertical resize ' .. cfg.get_window_width())