diff --git a/README.md b/README.md index 0a8005a..0573112 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ I do not merge PRs from the original repo that I don't personally need. - Fold all operation too slow: simrat39/symbols-outline.nvim#223 (simrat39/symbols-outline.nvim#224) - "Invalid buffer id" error simrat39/symbols-outline.nvim#177 - Open handler triggering multiple times ends up in messy state with errors -simrat39/symbols-outline.nvim#235 + simrat39/symbols-outline.nvim#235 +- Fixed `_highlight_current_item` function checking provider on outline window ## 🛑 Breaking changes @@ -74,6 +75,7 @@ features: - **Behaviour**: For `auto_preview=true`, previously preview is only shown after some delay. Now preview is shown instantly every time the cursor moves. + ## Features [Skip this section](#symbols-outlinenvim) @@ -107,6 +109,11 @@ Features/Changes: - Auto jump config option (see config `auto_goto`) (simrat39/symbols-outline.nvim#229, simrat39/symbols-outline.nvim#228). +- New Follow command, opposite of `goto_location`/`focus_location` + +- New restore location keymap option to go back to corresponding outline + location synced with code (see config `restore_location`). + ## PRs [Skip this section](#symbols-outlinenvim) @@ -193,7 +200,7 @@ Key: (#101 by druskus20) -### TODO +## TODO [Skip this section](#symbols-outlinenvim) @@ -225,9 +232,12 @@ Items will be moved to above list when complete. (simrat39/symbols-outline#128) - `[/]` Configurable width and height of preview window (simrat39/symbols-outline#130) -- `[/]` Outline window customizations (simrat39/symbols-outline#137) +- View + - `[/]` Outline window customizations (simrat39/symbols-outline#137) + - `[/]` Option to show line number next to symbols -### Related plugins + +## Related plugins - nvim-navic - nvim-navbuddy @@ -403,19 +413,25 @@ local opts = { -- Jump to symbol under cursor but keep focus on outline window. -- Renamed in this fork! peek_location = "o", + -- Only in this fork: + -- Change cursor position of outline window to the current location in code. + -- "Opposite" of goto/peek_location. + restore_location = "", + -- Open LSP/provider-dependent symbol hover information hover_symbol = "", - -- Preview symbol under cursor + -- Preview location code of the symbol under cursor toggle_preview = "K", + -- Symbol actions rename_symbol = "r", code_actions = "a", -- These fold actions are collapsing tree nodes, not code folding fold = "h", + unfold = "l", fold_toggle = '', -- Only in this fork -- Toggle folds for all nodes. -- If at least one node is folded, this action will fold all nodes. -- If all nodes are folded, this action will unfold all nodes. fold_toggle_all = '', -- Only in this fork - unfold = "l", fold_all = "W", unfold_all = "E", fold_reset = "R", @@ -504,6 +520,20 @@ local opts = { Display current provider and outline window status in the messages area. +- **:SymbolsOutlineFollow[!]** + + Go to corresponding node in outline based on cursor position in code, and + focus on the outline window. + + With bang, retain focus on the code window. + + This can be understood as the converse of `goto_location` (see keymaps). + `goto_location` sets cursor of code window to the position of outline window, + whereas this command sets position in outline window to the cursor position of + code window. + + With bang, it can be understood as the converse of `focus_location`. + ### Lua API @@ -516,13 +546,13 @@ require'symbols-outline' Toggle opening/closing of outline window. - If `opts.bang` is true, keep focus on previous window. + If `opts.focus_outline=false`, keep focus on previous window. - **open_outline(opts)** Open the outline window. - If `opts.bang` is true, keep focus on previous window. + If `opts.focus_outline=false`, keep focus on previous window. - **close_outline()** @@ -548,22 +578,36 @@ require'symbols-outline' Display current provider and outline window status in the messages area. +- **has_provider()** + + Returns whether a provider is available for current window. + +- **follow_cursor(opts)** + + Go to corresponding node in outline based on cursor position in code, and + focus on the outline window. + + With `opts.focus_outline=false`, cursor focus will remain on code window. + ## Default keymaps +These mappings are active for the outline window. + | Key | Action | | ---------- | -------------------------------------------------- | | Escape | Close outline | | ? | Show help message | | Enter | Go to symbol location in code | | o | Go to symbol location in code without losing focus | +| Ctrl+g | Go to code location in outline window | | Ctrl+Space | Hover current symbol | | K | Toggles the current symbol preview | | r | Rename symbol | | a | Code actions | -| h | fold symbol | -| Tab | toggle fold under cursor | -| Shift+Tab | toggle all folds | +| h | Fold symbol or parent symbol | +| Tab | Toggle fold under cursor | +| Shift+Tab | Toggle all folds | | l | Unfold symbol | | W | Fold all symbols | | E | Unfold all symbols | diff --git a/lua/symbols-outline/config.lua b/lua/symbols-outline/config.lua index a74a7ea..9698a74 100644 --- a/lua/symbols-outline/config.lua +++ b/lua/symbols-outline/config.lua @@ -37,6 +37,7 @@ M.defaults = { close = { '', 'q' }, goto_location = '', peek_location = 'o', + restore_location = "", hover_symbol = '', toggle_preview = 'K', rename_symbol = 'r', diff --git a/lua/symbols-outline/init.lua b/lua/symbols-outline/init.lua index 950e3f4..e929b1a 100644 --- a/lua/symbols-outline/init.lua +++ b/lua/symbols-outline/init.lua @@ -39,31 +39,6 @@ local function setup_global_autocmd() }) end -local function setup_buffer_autocmd() - if config.options.auto_preview then - vim.api.nvim_create_autocmd('CursorMoved', { - buffer = 0, - callback = require('symbols-outline.preview').show, - }) - else - vim.api.nvim_create_autocmd('CursorMoved', { - buffer = 0, - callback = require('symbols-outline.preview').close, - }) - end -end - -local function setup_commands() - local cmd = function(n, c, o) vim.api.nvim_create_user_command('SymbolsOutline'..n, c, o) end - cmd('', M.toggle_outline, { nargs = 0, bang = true }) - cmd('Open', M.open_outline, { nargs = 0, bang = true }) - cmd('Close', M.close_outline, { nargs = 0 }) - cmd('FocusOutline', M.focus_outline, { nargs = 0 }) - cmd('FocusCode', M.focus_code, { nargs = 0 }) - cmd('Focus', M.focus_toggle, { nargs = 0 }) - cmd('Status', M.show_status, { nargs = 0 }) -end - ------------------------- -- STATE ------------------------- @@ -71,7 +46,6 @@ M.state = { outline_items = {}, flattened_outline_items = {}, code_win = 0, - opts = {} } local function wipe_state() @@ -91,7 +65,9 @@ local function _merge_items(items) end local function __refresh() - if M.view:is_open() then + local current_buffer_is_outline = M.view.bufnr + == vim.api.nvim_get_current_buf() + if M.view:is_open() and current_buffer_is_outline then local function refresh_handler(response) if response == nil or type(response) ~= 'table' then return @@ -153,25 +129,6 @@ function M._toggle_fold(move_cursor, node_index) end end -local function setup_buffer_autocmd() - if config.options.auto_preview then - vim.api.nvim_create_autocmd('CursorHold', { - buffer = 0, - callback = require('symbols-outline.preview').show, - }) - else - vim.api.nvim_create_autocmd('CursorMoved', { - buffer = 0, - callback = function() - require('symbols-outline.preview').close() - if config.options.auto_goto then - M._goto_location(false) - end - end, - }) - end -end - local function setup_buffer_autocmd() if config.options.auto_preview then vim.api.nvim_create_autocmd('CursorMoved', { @@ -252,32 +209,25 @@ function M._set_all_folded(folded, nodes) end function M._highlight_current_item(winnr) - local has_provider = providers.has_provider() - - local is_current_buffer_the_outline = M.view.bufnr + local has_provider = M.has_provider() + local has_outline_open = M.view:is_open() + local current_buffer_is_outline = M.view.bufnr == vim.api.nvim_get_current_buf() - local doesnt_have_outline_buf = not M.view.bufnr - - local should_exit = not has_provider - or doesnt_have_outline_buf - or is_current_buffer_the_outline - - -- Make a special case if we have a window number - -- Because we might use this to manually focus so we dont want to quit this - -- function - if winnr then - should_exit = false + if not has_provider then + return end - if should_exit then + if + not has_outline_open -- Outline not open + -- Not currently on outline window, but no code window is given. + or (not winnr and not current_buffer_is_outline) + then return end local win = winnr or vim.api.nvim_get_current_win() - local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1 - local leaf_node = nil local cb = function(value) @@ -318,6 +268,10 @@ local function setup_keymaps(bufnr) map(config.options.keymaps.peek_location, function() M._goto_location(false) end) + -- Navigate to corresponding outline location for current code location + map(config.options.keymaps.restore_location, function() + M._map_follow_cursor() + end) -- Move down/up in outline and peek that location in code map(config.options.keymaps.down_and_goto, function() M._move_and_goto('down') @@ -381,7 +335,7 @@ local function setup_keymaps(bufnr) end) end -local function handler(response) +local function handler(response, opts) if response == nil or type(response) ~= 'table' or M.view:is_open() then return end @@ -408,42 +362,122 @@ local function handler(response) M._highlight_current_item(M.state.code_win) - if not config.options.focus_on_open or (M.state.opts and M.state.opts.bang) then + if not config.options.focus_on_open or (opts and not opts.focus_outline) then vim.fn.win_gotoid(M.state.code_win) end end +---Set position of outline window to match cursor position in code, return +---whether the window is just newly opened (previously not open). +---@param opts table? 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. +---@return boolean ok Whether it was successful. If ok=false, either the outline window is not open or the code window cannot be found. +function M.follow_cursor(opts) + if not M.view:is_open() then + return false + end + + if require('symbols-outline.preview').has_code_win() then + M._highlight_current_item(M.state.code_win) + else + return false + end + + if not opts then + opts = { focus_outline = true } + end + if opts.focus_outline then + M.focus_outline() + end + + return true +end + +local function _cmd_follow_cursor(opts) + local fnopts = { focus_outline = true } + if opts.bang then + fnopts.focus_outline = false + end + M.follow_cursor(fnopts) +end + +function M._map_follow_cursor() + if not M.follow_cursor({ focus_outline = true }) then + vim.notify( + "Code window no longer active. Try closing and reopening the outline.", + vim.log.levels.ERROR + ) + end +end + +---Toggle the outline window, and return whether the outline window is open +---after this operation. +---@param opts table? Table of options, @see open_outline +---@return boolean is_open Whether outline window is open function M.toggle_outline(opts) if M.view:is_open() then M.close_outline() + return false else M.open_outline(opts) + return true end end +-- Used for SymbolsOutline user command +local function _cmd_toggle_outline(opts) + if opts.bang then + M.toggle_outline({ focus_outline = false }) + else + M.toggle_outline({ focus_outline = true }) + end +end + +---Open the outline window. +---@param opts table? Field focus_outline=false means don't focus on outline window after opening. If opts is not provided, focus will be on outline window after opening. function M.open_outline(opts) + if not opts then + opts = { focus_outline = true } + end if not M.view:is_open() then - M.state.opts = opts - providers.request_symbols(handler) + providers.request_symbols(handler, opts) end end +local function _cmd_open_outline(opts) + if opts.bang then + M.open_outline({ focus_outline = false }) + else + M.open_outline({ focus_outline = true }) + end +end + +---Close the outline window. function M.close_outline() M.view:close() end +---Set cursor to focus on the outline window, return whether the window is currently open.. +---@return boolean is_open Whether the window is open function M.focus_outline() if M.view:is_open() then vim.fn.win_gotoid(M.view.winnr) + return true end + return false end +---Set cursor to focus on the code window, return whether this operation was successful. +---@return boolean ok Whether it was successful. If unsuccessful, it might mean that the attached code window has been closed or is no longer valid. function M.focus_code() if require('symbols-outline.preview').has_code_win() then vim.fn.win_gotoid(M.state.code_win) + return true end + return false end +---Toggle focus between outline and code window, returns whether it was successful. +---@return boolean ok Whether it was successful. If `ok=false`, either the outline window is not open or the code window is no longer valid. function M.focus_toggle() if M.view:is_open() and require('symbols-outline.preview').has_code_win() then local winid = vim.fn.win_getid() @@ -452,27 +486,80 @@ function M.focus_toggle() else vim.fn.win_gotoid(M.state.code_win) end + return true end + return false end +---Whether the outline window is currently open. +---@return boolean is_open function M.is_open() return M.view:is_open() end +---Display outline window status in the message area. function M.show_status() - if providers.has_provider() and _G._symbols_outline_current_provider then + if M.has_provider() then print("Current provider:") - print(_G._symbols_outline_current_provider.name) + print(' ' .. _G._symbols_outline_current_provider.name) if M.view:is_open() then - print("Outline window is open") + print("Outline window is open.") else - print("Outline window is not open") + print("Outline window is not open.") + end + if require('symbols-outline.preview').has_code_win() then + print("Code window is active.") + else + print("Warning: code window is either closed or invalid. Please close and reopen the outline window.") end else print("No providers") end end +---Whether there is currently an available provider. +---@return boolean has_provider +function M.has_provider() + local winid = vim.fn.win_getid() + if M.view:is_open() and winid == M.view.winnr then + return _G._symbols_outline_current_provider ~= nil + end + return providers.has_provider() and _G._symbols_outline_current_provider +end + +local function setup_commands() + local cmd = function(n, c, o) + vim.api.nvim_create_user_command('SymbolsOutline'..n, c, o) + end + + cmd('', _cmd_toggle_outline, { + desc = "Toggle the outline window. \ +With bang, keep focus on initial window after opening.", + nargs = 0, + bang = true, + }) + cmd('Open', _cmd_open_outline, { + desc = "With bang, keep focus on initial window after opening.", + nargs = 0, + bang = true, + }) + cmd('Close', M.close_outline, { nargs = 0 }) + cmd('FocusOutline', M.focus_outline, { nargs = 0 }) + cmd('FocusCode', M.focus_code, { nargs = 0 }) + cmd('Focus', M.focus_toggle, { nargs = 0 }) + cmd('Status', M.show_status, { + desc = "Show a message about the current status of the outline window.", + nargs = 0, + }) + cmd('Follow', _cmd_follow_cursor, { + desc = "Update position of outline with position of cursor. \ +With bang, don't switch cursor focus to outline window.", + nargs = 0, + bang = true, + }) +end + +---Set up configuration options for symbols-outline. function M.setup(opts) config.setup(opts) ui.setup_highlights() diff --git a/lua/symbols-outline/providers/coc.lua b/lua/symbols-outline/providers/coc.lua index 0b3e11f..c72e2c5 100644 --- a/lua/symbols-outline/providers/coc.lua +++ b/lua/symbols-outline/providers/coc.lua @@ -83,11 +83,12 @@ local function convert_symbols(result) end ---@param on_symbols function -function M.request_symbols(on_symbols) +---@param opts table +function M.request_symbols(on_symbols, opts) vim.fn.call('CocActionAsync', { 'documentSymbols', function(_, symbols) - on_symbols { [1000000] = { result = convert_symbols(symbols) } } + on_symbols({ [1000000] = { result = convert_symbols(symbols) } }, opts) end, }) end diff --git a/lua/symbols-outline/providers/init.lua b/lua/symbols-outline/providers/init.lua index c37cab6..dab6ee7 100644 --- a/lua/symbols-outline/providers/init.lua +++ b/lua/symbols-outline/providers/init.lua @@ -21,13 +21,13 @@ function M.has_provider() end ---@param on_symbols function -function M.request_symbols(on_symbols) +function M.request_symbols(on_symbols, opts) for _, value in ipairs(providers) do local provider = require(value) if provider.should_use_provider(0) then _G._symbols_outline_current_provider = provider _G._symbols_outline_current_provider.name = value - provider.request_symbols(on_symbols) + provider.request_symbols(on_symbols, opts) break end end diff --git a/lua/symbols-outline/providers/markdown.lua b/lua/symbols-outline/providers/markdown.lua index b1ffba3..3e531fd 100644 --- a/lua/symbols-outline/providers/markdown.lua +++ b/lua/symbols-outline/providers/markdown.lua @@ -17,8 +17,9 @@ function M.hover_info(_, _, on_info) end ---@param on_symbols function -function M.request_symbols(on_symbols) - on_symbols(md_parser.handle_markdown()) +---@param opts table +function M.request_symbols(on_symbols, opts) + on_symbols(md_parser.handle_markdown(), opts) end return M diff --git a/lua/symbols-outline/providers/nvim-lsp.lua b/lua/symbols-outline/providers/nvim-lsp.lua index 78d259c..05ff8cf 100644 --- a/lua/symbols-outline/providers/nvim-lsp.lua +++ b/lua/symbols-outline/providers/nvim-lsp.lua @@ -69,13 +69,14 @@ function M.postprocess_symbols(response) end ---@param on_symbols function -function M.request_symbols(on_symbols) +function M.request_symbols(on_symbols, opts) vim.lsp.buf_request_all( 0, 'textDocument/documentSymbol', getParams(), function (response) - on_symbols(M.postprocess_symbols(response)) + response = M.postprocess_symbols(response) + on_symbols(response, opts) end ) end